Iterable, Iterator 그리고 Generator

JunYoungK·2024년 9월 26일
1

AWS Glue에서 spark를 통해 최대 4억 행의 랜덤 데이터들을 생성하면서, 메모리 부족으로 계속 워커노드가 죽어버리는 문제가 발생한적이 있었다.
이를 해결하기위해 알아보던 중 Generator을 활용할 수 있다는 것을 알게되었고, 이를 활용하고자 했었다.
결과적으로는 다른 방법을 찾아 Generator를 프로젝트에 사용하지는 않았지만, 그래도 정리해두면 요긴하게 쓰일 것 같아 정리해보았다.


1. Iterable과 Iterator

1.1. Iterable

Iterable은 반복 가능한 객체를 의미하여, 내부 요소를 하나씩 차례로 반환할 수 있는 객체임

Iterable에는 리스트, 문자열, 딕셔너리, 튜플, 집합과 같은 자료 유형이 해당함

내부에 __iter__ 메서드가 있는 모든 객체라고 볼 수 있고, 재사용이 가능함

1.2. Iterator

Iterator는 Iterable한 객체의 요소를 순차적으로 접근할 수 있는 객체

Iterable 객체에서 __iter__ 메소드를 통해 생성되는 객체.

__next__ 메소드를 가지고 있어 이를 통해 다음 요소를 반환함

상태가 존재하는 객체로 한 번 순회하면 사용할 수 없음

1.3. 차이점

Iterable 객체의 경우 순회를 당하는 객체이고, Iterator 객체는 순회를 주관하는 객체로 볼 수 있음
또한, Iterable 객체는 순회 후 다시 순회할 수 있지만, Iterator는 한 번 순회하면 또 순회할 수 없음.
재순회를 위해서는 새로 Iterator를 선언해주어야함.

예시를 통해 보자

  • 다음은 Iterable 객체인 리스트를 순회한다.
    # iterable 객체인 리스트
    list1 = [1, 2, 3, 4]
    
    for num in list1:
    	print(num)
    
    for num in list1:
    	print(num)
    
    # 결과 값
    1
    2
    3
    4
    1
    2
    3
    4
    1. for문에 Iterable 객체를 넣어주면 내부의 __iter__ 메소드를 통해 임의의 Iterator를 생성
    2. 만들어진 Iterator 내부의 __next__ 메서드를 통해 Iterator의 끝까지 값이 반환됨
    3. Iterator의 마지막에 도달하면 StopIteration 에러를 발생시킴
    4. StopIteration 에러가 발생하면 for문을 종료
    5. 다시 for문을 통해 순회 가능
  • 위의 코드를 iterator를 사용하도록 수정하면 다음과 같음
    list1 = [1, 2, 3, 4]
    iterator1 = list1.__iter__()
    
    iterator1.__next__()
    iterator1.__next__()
    iterator1.__next__()
    iterator1.__next__()
    # iterator1.__next__() # StopIteration 에러를 발생시킴
    
    iterator1 = list1.__iter__()
    iterator1.__next__()
    iterator1.__next__()
    iterator1.__next__()
    iterator1.__next__()
    # iterator1.__next__() # StopIteration 에러를 발생시킴
    
    # 결과 값
    1
    2
    3
    4
    1
    2
    3
    4
    • 위의 예시와 달리 Iterator를 두번 선언한 것을 볼 수 있음

2. Generator

제너레이터는 yield 문을 이용하여 Iterator를 만들어내는 함수
함수 내에서 yield를 사용하면 yield 사용 여부를 떠나서 그 함수의 타입은 gererator가 됨

2.1. 생성하기

제너레이터는 yield 구문을 이용한 함수를 선언하거나, 제너레이터 컴프리헨션이라는 것을 통해 생성할 수 있음
제너레이터 컴프리헨션은 리스트의 것과 비슷하게 "(값 반복표현식 필요시 조건문)"를 사용하면 됨

def generator1(iterable):
    for i in iterable:
        yield i

gen1 = generator1(range(3))
gen2 = (i for i in range(3))

2.2. yield

yield는 해당 라인을 실행하고, yield가 있는 함수를 호출한 쪽으로 프로그램의 제어를 넘겨줌

예시를 통해 이해해보자

def generator1():
    for i in range(3):
        yield i + 1
        print(i + 1, "번째 실행")
    return 'Done'

gen = generator1()

print(next(gen))
print(next(gen))
print(next(gen))
# 3번 이후 실행시에는 더이상 순회할 수 없으므로 StopIteration 에러가 발생
try:
    print(next(gen))
except StopIteration as e:
    print(e.value) # 이때 generator1에서의 return 값이 반환됨

실행결과 =======================================================
1         # 첫번째 next()에서 출력
1 번째 실행 # 두번째 next()에서 출력
2         # 두번째 next()에서 출력    
2 번째 실행 # 세번째 next()에서 출력
3         # 세번째 next()에서 출력
3 번째 실행 # 네번째 next()시 출력되고, 함수 내 반복문 종료로 StopIteration 에러 발생
Done      # 얘외 처리로 인해 출력
  • 위의 예시를 보면 next()를 통해 제너레이터인 gen을 호출하면, 1부터 값이 출력되고, 다시 호출하면 1이 아닌 2가 출력되고, 결국 3까지 출력됨을 볼 수 있다.
  • 이처럼 yield는 값을 호출한 곳으로 넘겨준 후, 자신의 상태를 기억하여 다음에 또 호출될시 yield 아래 문구부터 다시 수행되는 것을 알 수 있다.

한번 return으로 동일하게 작성해서 해보자

def return_func():
    for i in range(3):
        return i + 1

var = return_func()
print(type(var))
print(next(var))

실행결과 =======================================================
<class 'int'>
Traceback (most recent call last):
  File "/Users/junyoung-kim/.../test2.py", line 25, in <module>
    print(next(var))
          ^^^^^^^^^
TypeError: 'int' object is not an iterator
  • return은 그대로 숫자 1의 값을 넘겨주어 iterator가 아닌 int 타입임을 확인할 수 있고,
    이에 따라 next()호출시 오류를 발생시킨다.

2.3. 왜 쓸까?

yield 구문을 통해 생성하는 generator는 그럼 왜 사용하는 것일까?

이는 이 generator의 특성을 보면 알 수 있다.

위에서 yield는 호출 될 때마다 값을 하나씩 넘겨주고, 또 호출되면 다음 순서의 값을 넘겨주는 특성을 가지고 있다.

이는 리스트와 같은 순회가능한 자료형을 생성하여 통째로 return하는 것보다 iterable 객체 내부의 값을 하나씩 넘겨주는 것으로 메모리 공간에서 큰 이점을 얻을 수 있다.

from sys import getsizeof

def generator1():
    for i in range(1_000_000):
        yield i + 1
        print(i + 1, "번째 실행")
    return 'Done'

def get_list():
    return [i for i in range(1_000_000)]

print(getsizeof(generator1()))
print(getsizeof(get_list()))

실행결과 =======================================================
216     # size of Generator
8448728 # size of returning entire iterable object

이처럼 대용량의 데이터를 이용하고자할 때에는 순회 데이터의 값을 하나씩 넘겨주는 generator가 훨씬 메모리면에서 효율적인 것을 알 수 있다.
하지만, 수행 시간 면에서는 generator가 더 느리니, 처리 데이터로 인한 메모리 부족이 일어나지 않는 환경에서는 정말 사용을 해야하는 것인지 한 번 더 생각해 볼 필요가 있겠다.

6개의 댓글

comment-user-thumbnail
2024년 10월 16일

포스팅 잘 읽었습니다! 앞서 스파크에서 랜덤 데이터를 생성하며 메모리 부족으로 워커노드가 자꾸 죽는 현상이 발생했다고 언급해 주셨는데, 그 코드를 어떻게 수정할 수 있는지 궁금해요

1개의 답글
comment-user-thumbnail
2024년 10월 16일

포스트글 잘 읽었습니다. 더 깊게 생각할게 무엇이 있을지 아래에 더 적어보겠습니다.
1. AWS Glue 환경에서 메모리 부족 문제를 해결하기 위한 다른 접근 방법은 어떤 것이 있을까
2. Iterator와 Generator의 내부 동작 원리를 Python의 메모리 관리 측면에서 깊게 이해해볼 수 있을까(가비지 컬렉션과 같은 개념)

1개의 답글
comment-user-thumbnail
2024년 10월 16일

포스팅 잘 읽었습니다!
Iterator와 Generator에 대한 개념만 알고 있었는데 메모리 용량과 수행 속도에 대해서는 처음 생각해보게 되었습니다.

1개의 답글