제너레이터

이전 문서에서는 클래스로 이터레이터를 만들어보았다. 제너레이터는 이터레이터를 아주 간편하게 만드는 함수이다.

  • 함수안에 yield를 사용한다. (return 과 비교)
  • 제너레이터 표현식을 사용할 수 있다.
  • 메모리 사용이 효율적이다.

제너네이터 사용하기

def season_generator(*args):
    for arg in args:
        yield arg
        
g = season_generator('spring', 'summer', 'autumn', 'winter')

yield는 첫번째 arg 값을 밖으로 리턴을 해주고 함수 실행을 잠깐 지연시키는 역할을 한다. 다음 __next__값이 오면 다음 yield값을 리턴해 준다.

>>> print(g)
<generator object season_generator at 0x7fe1a40c3e40>

>>> print(dir(g))
['__class__', '__del__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__lt__', '__name__', '__ne__', '__new__', '__next__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'close', 'gi_code', 'gi_frame', 'gi_running', 'gi_yieldfrom', 'send', 'throw']

이제 g는 제너레이터 객체이다. 제너레이터는 이터레이터 안에 포함되어 있기 때문에 이터레이터 처럼 __next__함수를 사용할 수 있다.

>>> print(g.__next__())
spring
>>> print(g.__next__())
summer
>>> print(g.__next__())
autumn
>>> print(g.__next__())
winter
>>> print(g.__next__())
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

return vs yield

return은 작업 도중에 만나면 바로 함수 실행이 종료되고 결과 값을 반환해 준다. 반면에 yield는 작업을 나눠서 관리해줄 수 있다.

def func():
    print("첫번째 작업 중...")
    return 1

    print("두번째 작업 중...")
    yield 2

    print("세번째 작업 중...")
    yield 3
    
ge = func()
>>> data = ge.__next__()
첫번째 작업 중...
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration: 1
def func():
    print("첫번째 작업 중...")
    yield 1

    print("두번째 작업 중...")
    yield 2

    print("세번째 작업 중...")
    yield 3
    
ge = func()
>>> data = ge.__next__()
첫번째 작업 중...
>>> print(data)
1
>>> data = ge.__next__()
두번째 작업 중...
>>> print(data)
2
>>> data = ge.__next__()
세번째 작업 중...
>>> print(data)
3

제너레이터 표현식

제너레이터 표현식은 리스트를 만드는 것과 유사하다. 리스트를 만들 때 사용하는 []를 ()로 바꿔주면 끝난다.

double_generator = (i * 2 for i in range(1,10))

for i in double_generator:
    print(i)
  
2
4
6
8
10
12
14
16
18

이렇게 함수를 만들지 않고서도 간단하게 제너레이터를 만들 수 있다.

메모리 사용 확인

숫자 1 - 10000 3배로 만든 결과 리스트 vs 제너레이터

  • 리스트 : 데이터 저장에 필요한 메모리를 모두 사용
  • 제너레이터 : 나중에 필요할 때 값을 만들어 사용 (메모리 매우 적게필요)
import sys

list_data = [i * 3 for i in range(1, 10000 + 1)]
generator_data = (i * 3 for i in range(1, 10000 + 1))
>>> print(sys.getsizeof(list_data))
87616
>>> print(sys.getsizeof(generator_data))
112

리스트는 876161 바이트 만큼의 크기를 차지하는데 이는 결과값을 메모리상에 저장하고 있기 때문이다. 반면에 제너레이터는 식만 만들 뿐 결과값은 저장하지 않고 __next__ 함수가 호풀될 때마다 결과값을 만든다. 따라서 대용량의 데이터를 사용할 때에는 리스트 보다는 제너레이터를 사용하는 것이 좋다.