Python에서 제너레이터(generator)는 이터레이터를 생성하는 강력한 도구입니다. 이 문서에서는 제너레이터의 개념부터 활용 방법까지 자세히 알아보겠습니다.
제너레이터 개념
제너레이터는 결과를 한 번에 리스트에 저장하지 않습니다. 대신 생성기의 상태를 유지하면서每次迭代 시 값을 반환하며, StopIteration 예외가 발생할 때까지 동작합니다. 이렇게 하면 메모리를 효율적으로 사용할 수 있습니다.
제너레이터 표현식
제너레이터 표현식은 리스트 컴프리헨션과 유사한 문법을 사용합니다. 차이점은 대괄호 [] 대신 소괄호 ()를 사용한다는 점입니다. 처리해야 할 데이터 양이 클 때 리스트 컴프리헨션은 메모리를 많이 사용하지만, 제너레이터 표현식은 필요할 때만 값을 생성합니다.
>>> gen = (x**2 for x in range(5))
>>> gen
<generator object <genexpr> at 0x0000000002FB7B40>
>>> for g in gen:
... print(g, end='-')
...
0-1-4-9-16-
>>> for x in [0,1,2,3,4,5]:
... print(x, end='-')
...
0-1-2-3-4-5-
제너레이터 함수
함수 내에서 yield 키워드를 사용하면 해당 함수는 일반 함수가 아니라 제너레이터 함수가 됩니다. yield가 포함된 함수는 더 이상 일반 함수가 아니며, Python 인터프리터는 이를 제너레이터로 인식합니다.
다음은 무한하게 홀수를 생성하는 제너레이터 함수입니다.
def odd():
n=1
while True:
yield n
n+=2
odd_num = odd()
count = 0
for o in odd_num:
if count >=5: break
print(o)
count +=1
물론 수동으로 이터레이터를 구현하여 유사한 효과를 얻을 수 있지만, 제너레이터가 더 직관적이고 이해하기 쉽습니다.
<span class="hljs-keyword">class Iter:</span>
<span class="hljs-keyword">def __init__(self):</span>
self.start=-1
<span class="hljs-keyword">def __iter__(self):</span>
return self
<span class="hljs-keyword">def __next__(self):</span>
self.start +=2
return self.start
I = Iter()
for count in range(5):
print(next(I))
참고: 제너레이터에는 __iter__()와 __next__() 메서드가 포함되어 있으므로 for 루프로 직접 반복할 수 있습니다. StopIteration을 포함하지 않은 수동 이터레이터는 수동 루프로만 반복해야 합니다.
>>> from collections import Iterable
>>> from collections import Iterator
>>> isinstance(odd_num, Iterable)
True
>>> isinstance(odd_num, Iterator)
True
>>> iter(odd_num) is odd_num
True
>>> help(odd_num)
Help on generator object:
odd = class generator(object)
| Methods defined here:
|
| __iter__(self, /)
| Implement iter(self).
|
| __next__(self, /)
| Implement next(self).
......
위 결과를 보면, Iterator 방식으로 루프를 수행할 수 있습니다.
for 루프가 실행될 때마다 제너레이터 함수 내부 코드가 실행됩니다. yield b를 만나면 함수는 반복 값을 반환하고, 다음 반복에서는 yield b 다음 문장부터 계속 실행됩니다. 함수의 로컬 변수는 이전 중단 지점과 완전히 동일하게 유지되므로, 함수가 계속 실행되어 다시 yield를 만나게 됩니다. 이는 마치 함수가 실행 도중에 여러 번 중단된 것처럼 보입니다.
yield와 return의 차이
return이 없는 경우
제너레이터에 return 문이 없으면 함수가 끝까지 실행된 후 StopIteration을 발생시킵니다.
>>> def g1():
... yield 1
...
>>> g=g1()
>>> next(g)
1
>>> next(g)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
return이 있는 경우
실행过程中에서 return을 만나면 즉시 StopIteration을 발생시켜迭代을 종료합니다.
>>> def g2():
... yield 'a'
... return
... yield 'b'
...
>>> g=g2()
>>> next(g)
'a'
>>> next(g)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
return으로 값 반환하기
return 뒤에 값을 지정하면 해당 값은 StopIteration 예외의 설명이 됩니다. 제너레이터는 return으로 값을 반환할 수 없습니다.
>>> def g3():
... yield 'hello'
... return 'world'
...
>>> g=g3()
>>> next(g)
'hello'
>>> next(g)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration: world
제너레이터가 지원하는 메서드
>> help(odd_num)
Help on generator object:
odd = class generator(object)
| Methods defined here:
......
| close(...)
| close() -> raise GeneratorExit inside generator.
|
| send(...)
| send(arg) -> send 'arg' into generator,
| return next yielded value or raise StopIteration.
|
| throw(...)
| throw(typ[,val[,tb]]) -> raise exception in generator,
| return next yielded value or raise StopIteration.
......
close() 메서드
수동으로 제너레이터 함수를 닫습니다. close() 이후의 호출은 바로 StopIteration 예외를 반환합니다.
>>> def g4():
... yield 1
... yield 2
... yield 3
...
>>> g=g4()
>>> next(g)
1
>>> g.close()
>>> next(g)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
send() 메서드
제너레이터 함수의 가장 큰特点是 외부에서 값을 전달받을 수 있으며, 해당 값으로 계산한 결과를 반환합니다. 이것이 제너레이터 함수에서 가장 이해하기 어렵고 중요한 부분이며, 이후 설명할 코루틴(coroutine)을 구현하는 데 필수적입니다.
def gen():
value=0
while True:
receive=yield value
if receive=='e':
break
value = 'got: %s' % receive
g=gen()
print(g.send(None))
print(g.send('aaa'))
print(g.send(3))
print(g.send('e'))
실행流程:
- g.send(None) 또는 next(g)로 제너레이터를 시작하면 첫 번째 yield 문이 끝나는 위치까지 실행됩니다.此时 yield 문은 실행되었지만 receive에는 아직 값이 할당되지 않았습니다. yield value는 초기값 0을 출력합니다. 注意: 제너레이터 시작 시에는 send(None)만 사용할 수 있으며, 다른 값을 전달하면 오류가 발생합니다.
- g.send('aaa')를 호출하면 'aaa'가 전달되어 receive에 할당되고, value를 계산한 후 while 문 상단으로 돌아가 yield value 문에서 정지합니다. 此时 yield value는 "got: aaa"를 출력하고 일시 정지합니다.
- g.send(3)은 2번 과정을 반복하여 "got: 3"을 출력합니다.
- g.send('e')를 전달하면 프로그램이 break를 실행하여 루프를 종료하고, 함수가 완료되어 StopIteration 예외가 발생합니다.
실행 결과:
0
got: aaa
got: 3
Traceback (most recent call last):
File "h.py", line 14, in <module>
print(g.send('e'))
StopIteration
throw() 메서드
제너레이터 함수에 예외를 전달합니다. 시스템 정의 예외 또는 사용자 정의 예외를 사용할 수 있습니다. throw()는 예외를 발생시키고 프로그램을 종료하거나, 하나의 yield를 소모하거나, 다음 yield가 없을 때 프로그램 끝까지 실행됩니다.
def gen():
while True:
try:
yield 'normal value'
yield 'normal value 2'
print('here')
except ValueError:
print('we got ValueError here')
except TypeError:
break
g=gen()
print(next(g))
print(g.throw(ValueError))
print(next(g))
print(g.throw(TypeError))
출력 결과:
normal value
we got ValueError here
normal value
normal value 2
Traceback (most recent call last):
File "h.py", line 15, in <module>
print(g.throw(TypeError))
StopIteration
설명:
- print(next(g)): 'normal value'를 출력하고 yield 'normal value 2' 이전에 정지합니다.
- g.throw(ValueError)를 실행하면 모든后续 try 문이 건너뛰어지고 except 문으로 이동하여 'we got ValueError here'를 출력합니다. 그 다음 while 문으로 돌아가 한 개의 yield를 소모하므로 'normal value'를 출력합니다.
- print(next(g)): yield 'normal value 2' 문을 실행하고 해당 문 실행 후 위치에 정지합니다.
- g.throw(TypeError): try 문을 빠져나가 print('here')가 실행되지 않고 break 문으로 while 루프를 종료한 후 프로그램 끝에 도달하여 StopIteration 예외를 발생시킵니다.
응용 예제: 다중 리스트 평탄화
다음은 다중 리스트를 하나의 플랫 리스트로 변환하는 예제입니다.
def flatten(nested):
try:
if isinstance(nested, str):
raise TypeError
for sublist in nested:
for element in flatten(sublist):
print('got:', element)
except TypeError:
yield nested
L=['aaadf',[1,2,3],2,4,[5,[6,[8,[9]],'ddf'],7]]
for num in flatten(L):
print(num)
이해하기 어려우면 print 문 주석을 풀어서 확인하면 됩니다.
정리
- 덕 타이핑 이론에 따르면, 제너레이터는 이터레이터이므로 for 루프로迭代할 수 있습니다.
- 처음으로 next(generator)를 실행하면 yield 문까지 실행된 후 프로그램이 일시 정지되며, 모든 파라미터와 상태가 저장됩니다. 다시 next(generator)를 실행하면 일시 정지된 위치부터 계속 실행됩니다. 프로그램 끝이나 StopIteration을 만나면 루프가 종료됩니다.
- generator.send(arg)를 사용하여 파라미터를 전달할 수 있습니다. 이것이 코루틴 모델입니다.
- generator.throw(exception)을 사용하여 예외를 전달할 수 있습니다. throw 문은 하나의 yield를 소모합니다. generator.close()를 사용하여 제너레이터를 수동으로 닫을 수 있습니다.
- next()는 send(None)과 동일합니다.