Python 제너레이터의 작동 원리

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'))

실행流程:

  1. g.send(None) 또는 next(g)로 제너레이터를 시작하면 첫 번째 yield 문이 끝나는 위치까지 실행됩니다.此时 yield 문은 실행되었지만 receive에는 아직 값이 할당되지 않았습니다. yield value는 초기값 0을 출력합니다. 注意: 제너레이터 시작 시에는 send(None)만 사용할 수 있으며, 다른 값을 전달하면 오류가 발생합니다.
  2. g.send('aaa')를 호출하면 'aaa'가 전달되어 receive에 할당되고, value를 계산한 후 while 문 상단으로 돌아가 yield value 문에서 정지합니다. 此时 yield value는 "got: aaa"를 출력하고 일시 정지합니다.
  3. g.send(3)은 2번 과정을 반복하여 "got: 3"을 출력합니다.
  4. 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

설명:

  1. print(next(g)): 'normal value'를 출력하고 yield 'normal value 2' 이전에 정지합니다.
  2. g.throw(ValueError)를 실행하면 모든后续 try 문이 건너뛰어지고 except 문으로 이동하여 'we got ValueError here'를 출력합니다. 그 다음 while 문으로 돌아가 한 개의 yield를 소모하므로 'normal value'를 출력합니다.
  3. print(next(g)): yield 'normal value 2' 문을 실행하고 해당 문 실행 후 위치에 정지합니다.
  4. 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 문 주석을 풀어서 확인하면 됩니다.

정리

  1. 덕 타이핑 이론에 따르면, 제너레이터는 이터레이터이므로 for 루프로迭代할 수 있습니다.
  2. 처음으로 next(generator)를 실행하면 yield 문까지 실행된 후 프로그램이 일시 정지되며, 모든 파라미터와 상태가 저장됩니다. 다시 next(generator)를 실행하면 일시 정지된 위치부터 계속 실행됩니다. 프로그램 끝이나 StopIteration을 만나면 루프가 종료됩니다.
  3. generator.send(arg)를 사용하여 파라미터를 전달할 수 있습니다. 이것이 코루틴 모델입니다.
  4. generator.throw(exception)을 사용하여 예외를 전달할 수 있습니다. throw 문은 하나의 yield를 소모합니다. generator.close()를 사용하여 제너레이터를 수동으로 닫을 수 있습니다.
  5. next()는 send(None)과 동일합니다.

태그: python generator iterator yield coroutine

7월 16일 00:58에 게시됨