함수에서 yield
키워드를 사용하면 제너레이터라는 객체를 정의하게 된다. 제너레이터의 주된 기능은 반복에 사용할 값을 생성하는 것이다. 다음의 예를 살펴보자.
def countdown(n):
print('Counting down from', n)
while n > 0:
yield n
n -= 1
c = countdown(10)
print(c)
위의 코드를 실행하면 다음의 결과를 얻는다.
<generator object countdown at 0x7fb0ce79c110>
제너레이터 객체를 얻게되는데, 이는 yield
키워드로 생성된 제너레이터 객체로 그 때 당시의 값을 어떻게 호출할 수 있는 지 방법을 가지고 있다. 제너레이터는 반복을 시작할 때 함수를 실행한다. 제너레이터 객체를 수행하는 방법은 다음과 같이 next()
를 호출하는 것이다.
c = countdown(10)
print(next(c), next(c)) # 10 9
next
를 호출하면 제너레이터 함수는 yield
문까지 문장을 실행한다. yield
문은 결과를 반환하며 next()
가 다시 호출되기 전까지 함수 실행을 일시적으로 중단한다. 중단되는 동안 함수는 지역 변수와 실행 환경을 모두 유지한다. 함수가 재개되면 yield
다음에 오는 문장에서 실행을 계속한다.
next()
는 제너레이터의 __next__()
메서드를 호출하는 약어이다. 다음과 같이 수행도 가능하다.
c = countdown(10)
print(next(c), next(c), c.__next__(), c.__next__()) # 10 9 8 7
보통 제너레이터에서 next()
를 직접 호출할 일은 없고, 항목을 처리하는 for
문 또는 특정 연산에서 사용한다. 다음의 예를 살펴보자.
for n in countdown(10):
...
a = sum(countdown(10))
제너레이터 함수는 함수의 끝에 도달하거나 return
문으로 반환되기 전까지 항목을 생성한다. for
루프가 종료되면 StopIteration
예외가 발생한다. 제너레이터 함수가 None
이 아닌 값을 반환한다면 StopIteration
예외와 함께 반환된다. 다음 예제 코드에서 제너레이터는 yield
와 return
을 모두 사용한다.
def func():
yield 37
return 42
f = func()
print(next(f)) # 37
print(next(f)) # StopIteration: 42
반환값이 StopIteration
과 함께 있는 것을 볼 수 있다. except
로 예외를 걸러내고 값을 가져올 수 있다.
try:
print(next(f)) # 37
except StopIteration as e:
print(e.value) # 42
일반적으로 제너레이터 함수는 값을 반환하지 않는다. 제너레이터는 예외값(exception value)을 얻을 방법이 없는 for
루프에서 대부분 처리된다. 이것은 값을 얻는 유일한 방법이 명시적으로 next()
를 호출하여 수동으로 제너레이터를 실행하는 것 뿐임을 의미한다. 제너레이터와 관련된 대부분의 코드가 꼭 이렇진 않다.
제너레이터가 부분적으로만 실행되는 경우, 제너레이터에서 미묘한 문제가 발생한다. 가령 다음 예시는 반복에서 일찍 빠져나오는 코드이다.
def countdown(n):
print('Counting down from', n)
while n > 0:
yield n
n -= 1
for x in countdown(10):
print(x)
if x == 2:
break
결과는 다음과 같다.
Counting down from 10
10
9
8
7
6
5
4
3
2
위 예에서 break
가 호출되면 for
루프가 중단되고 제너레이터는 끝까지 실행되지 않는다. 제너레이터에서 일종의 정리 작업을 수행하는 게 중요하다면 try-finally
또는 컨텍스트 관리자를 사용해야 한다.
def countdown(n):
print('Counting down from', n)
try:
while n > 0:
yield n
n -= 1
finally:
print("Only made it to", n)
for x in countdown(10):
print(x)
if x == 2:
break
결과는 다음과 같다.
Counting down from 10
10
9
8
7
6
5
4
3
2
Only made it to 2
위 코드에서 제너레이터가 모두 실행되지 않아도 finally
블록 코드는 실행이 보장된다. 버려진 제너레이터는 가비지 컬렉션 될 때 실행된다. 마찬가지로 컨텍스트 관리자와 정리 작업 코드도 제너레이터가 종료될 때 실행을 보장한다.
def func(filename):
with open(filename) as file:
...
yield data
...
# 제너레이터가 버려져도 파일은 여기서 닫힘
자원을 적절히 정리하는 것은 까다로운 문제이다. try-finally
또는 컨텍스트 관리자와 함께 제너레이터를 사용하는 한, 제너레이터는 일찍 종료되더라도 올바른 작업을 수행한다.
일반적으로 제너레이터는 다음과 같이 한 번만 실행된다.
def countdown(n):
while n > 0:
yield n
n -= 1
c = countdown(3)
for n in c:
print(n)
for n in c:
print(n)
결과는 다음과 같다.
3
2
1
두 번째 for
문에는 나오는 것이 없다. 이는 제너레이터 객체가 종료되었기 때문이다.
여러 번 반복할 수 있는 객체가 필요하다면, 이를 클래스로 정의하고 __iter__()
메서드를 제너레이터로 만들면 된다.
class countdown:
def __init__(self, start):
self.start = start
def __iter__(self):
n = self.start
while n > 0:
yield n
n -= 1
c = countdown(3)
for n in c:
print(n)
for n in c:
print(n)
결과는 다음과 같다.
3
2
1
3
2
1
yield
를 포함하는 함수는 스스로 실행되지 않는다는 점이 제너레이터의 중요한 특징이다. 제너레이터는 항상 for-loop
또는 명시적으로 next()
를 호출하는 다른 코드가 있어야 한다. 단순 일반 호출만으로 제너레이터를 실행하는 게 충분하지 않기 때문에 yield
를 포함하는 라이브러리 함수는 작성하기가 다소 까다롭다. 디를 해결하기 위해 yield from
문을 사용할 수 있다.
def countup(stop):
n = 1
while n <= stop:
yield n
n += 1
def countdown(start):
n = start
while n > 0:
yield n
n -= 1
def up_and_down(n):
yield from countup(n)
yield from countdown(n)
for x in up_and_down(5):
print(x, end=" ") # 1 2 3 4 5 5 4 3 2 1
yeild from
은 반복 프로세스를 외부 반복 객체에 효과적으로 위임한다.
1 2 3 4 5 5 4 3 2 1
yield from
은 반복 작업을 직접 수행하지 않도록 해준다. 이 기능이 없으면 up_and_down(n)
은 다음과 같이 작성해야한다.
def up_and_down(n):
for x in countup(n):
yield x
for x in countdown(n):
yield x
yield from
은 중첩된 반복 가능 객체를 재귀적으로 반복해야하는 코드를 작성할 때 특히 유용하다. 다음은 리스트를 중첩없이 평평하게 flatten하는 코드이다.
def flatten(items):
for i in items:
if isinstance(i, list):
yield from flatten(i)
else:
yield i
a = [1,2,[3,[4,5], 6 , 7], 8]
for x in flatten(a):
print(x, end=" ") # 1 2 3 4 5 6 7 8
이 구현의 한 가지 제약 사항은 여전히 파이썬의 재귀 제한(recursion limit)이 적용되므로 깊이 중첩된 구조는 처리할 수 없다.
제너레이터는 다른 복잡한 사용이 있지만 이는 추후에 알아보도록 하자.