Python generator (제네레이터)

Python generator에 대해 살펴보겠습니다.

generator란 간단하게 말하면 iterator를 리턴하는 함수를 의미합니다.

iterator는 데이터에 순차적 접근이 가능한 객체를 의미합니다.

 

1. 사용방법

generator함수를 만드는 방법은 yield 구문을 사용하는 것입니다.

일반 함수와 generator의 차이는 yield 구문뿐 입니다.

>>> def generatorA():
...     yield 'firstReturn'
...     yield 'secondReturn'
...     yield 'thirdReturn'

>>> genA = generatorA()
>>> type(genA)
<class 'generator'>

>>> next(genA)
'firstReturn'
>>> next(genA)
'secondReturn'
>>> next(genA)
'thirdReturn'
>>> next(genA)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

generator함수는 실행 중 yield를 만날 경우 중지되어 next()를 호출한 쪽으로 반환 값을 리턴합니다.

next()를 또 호출하면 중지된 부분부터 다시 시작되어서 다음 yield에 반환 값을 리턴합니다.

더 이상 yield가 없는 경우에는 오류가 발생합니다.

 

yield를 기준으로 순서가 생긴 generator 함수는 iterator로써 for문을 사용할 수 있습니다.

>>> def generatorA():
...     yield 'first'
...     yield 'second'
...     yield 'third'

>>> for i in generatorA():
...     i

'first'
'second'
'third'

 

generator expression을 제공하고 있습니다.

list comprehension과 비슷합니다, [ ] 대신 ( )를 사용합니다.

>>> [ i for i in range(5) ]
[0, 1, 2, 3, 4]

>>> ( i for i in range(5) )
<generator object <genexpr> at 0x0182A728>

 

일반 함수는 리턴시 반환 값을 호출부에 넘겨주고 종료되어 메모리상에서 지워집니다.

generator 함수는 실행 중 yield를 만나면 정지되어 반환 값을 호출부에 넘겨주지만

함수가 종료되지 않고 그 상태로 유지됩니다, 

즉 함수 내부의 변수 등 내부에서 사용된 데이터가 메모리에 그대로 유지되어 있습니다.

따라서 아래예시 처럼 무한한 순서가있는 iterator를 만들 수 있습니다.

>>> def double_generator():
...     num = 1
...     yield num
...     while True:
...         num * 2
...         yield num

>>> genA = double_generator()
>>> next(genA)
1
>>> next(genA)
2
>>> next(genA)
4
>>> next(genA)
8
>>> next(genA)
16
>>> next(genA)
32

netx()를 통해 중지되었지만 함수 내부에 사용된 변수 num의 값은 여전히 유지되어 있습니다.

 

2. genarator의 장점

generator의 장점은 memory를 효율적으로 사용한다는 것이다.

아래 예제를 통해 list와 generator의 메모리 사용을 비교해보자

>>> import sys
# list comprehension
>>> sys.getsizeof([ num for num in range(100) ])
452
>>> sys.getsizeof([ num for num in range(1000) ])
4508

# generator expression
>>> sys.getsizeof(( num for num in range(100) ))
56
>>> sys.getsizeof(( num for num in range(1000) ))
56

위 예제를보면 list의 경우 사이즈가 클수록 메모리 사용량도 커진다.

하지만 generator의 경우는 사이즈가 커짐에도 메모리 사용량은 그대로이다.

 

list는 list 안에 속한 모든 원소를 메모리에 적재하기에 list의 크기만큼 메모리가 늘어나게 된다.

generator의 경우 모든 데이터를 메모리에 적재하는 게 아니라 next() 함수를 통해 

값에 접근할 때마다 메모리에 적재하는 방식입니다.

 

그러므로 규모가 큰 iterator(list...)를 다를수록 generator의 효율성은 높아집니다.

 

3. yield from

yield from 은 python 3.3 이상부터 지원하는 문법입니다.

기존에는 list처럼 iterable한 객체의 원소를 전달할 때 for문을 사용해야 했습니다.

iterable한 객체를 yield 할 때 yield from 문법을 통해 for문 없이 값을 전달할 수 있게 되었습니다.

# 기존.. for문사용
>>> def generatorA():
...     for i in [1, 3, 5]:
...         yield i

>>> for i in generatorA():
...     i
1
3
5

# yield from 사용
>>> def generatorB():
...     yield from [1, 3, 5]

>>> for i in generatorB():
...     i
1
3
5

 

factorial(팩토리얼)이나, fibonacci(피보나치) 수열 등 수학공식 등을 코딩하는데

매우 효율적으로 쓰이는 것을 보았습니다.

외에도 적절히 사용하면 굉장히 효율적이고 파워풀한 코드가 될 수 있을 것 같다.