2.31. 이터레이터와 제너레이터

for 루프는 보기보다 더 많은 일을 해 왔습니다. 이 페이지에서는 그것이 동작하는 기반인 이터레이터 프로토콜(iterator protocol), 그리고 직접 이터레이터를 만들 수 있게 해 주는 yield 키워드를 다룹니다.

2.31.1. 이터레이터 프로토콜

반복할 수 있는 모든 객체는 두 가지 메서드를 구현합니다:

  • __iter__() – 객체의 항목들에 대한 이터레이터(iterator) 를 반환합니다.

  • __next__() – 이터레이터에서 다음 항목을 반환하거나, 더 이상 없으면 StopIteration 을 발생시킵니다.

iter() 내장 함수는 __iter__ 를 호출하고, next()__next__ 를 호출합니다. 리스트를 직접 하나씩 따라가 봅시다:

it = iter([10, 20, 30])
print(next(it))    # 10
print(next(it))    # 20
print(next(it))    # 30
print(next(it))    # raises StopIteration
for 루프는 __iter__ 를 한 번 호출해 이터레이터를 얻은 다음, StopIteration 이 루프를 끝낼 때까지 __next__ 를 반복 호출합니다.

for 는 “__iter__ 를 한 번 호출한 다음, StopIteration 이 날 때까지 __next__ 를 반복 호출하라”의 문법적 설탕입니다.

for x in items: 가 실제로 하는 일:

_it = iter(items)
while True:
    try:
        x = next(_it)
    except StopIteration:
        break
    # ... loop body ...

모든 리스트, 튜플, 문자열, 딕셔너리, 집합, 파일 객체, 제너레이터는 이미 __iter____next__ 를 구현하고 있습니다 – 그래서 이들 모두가 for 와 함께 동작하는 것입니다.

2.31.2. yield와 제너레이터 함수

yield 문을 포함하는 함수는 제너레이터 함수입니다. 이를 호출해도 본문이 실행되지 않으며, 본문을 한 번에 하나의 yield씩 실행하는 제너레이터 객체(반복자)를 반환합니다:

def count_up_to(n):
    i = 0
    while i < n:
        yield i
        i += 1

for value in count_up_to(3):
    print(value)

출력:

0
1
2

next() 를 호출할 때마다 함수는 다음 yield 까지 재개되어 그 값을 호출자에게 넘기고 거기서 일시 중단합니다. 지역 상태(이 경우 i)는 재개 사이에 보존됩니다.

두 개의 라이프라인(호출자와 제너레이터 본문)을 가진 시퀀스 다이어그램. 호출자가 count_up_to(3) 을 호출하면 본문을 실행하지 않은 채 제너레이터가 생성됩니다. 이후 매 next() 호출은 다음 yield 까지 본문을 실행하고 내보낸 값을 반환한 뒤 일시 중단합니다. 네 번째 next() 는 끝을 넘어가 StopIteration 을 발생시킵니다. 변수 i 는 일시 중단을 거쳐도 보존됩니다.

next() 는 다음 yield 까지 본문을 실행하고, 값을 돌려준 뒤 일시 중단합니다. 지역 상태는 중단을 거쳐도 살아남습니다.

제너레이터는 시퀀스를 지연 방식으로 생성하는 가장 쉬운 방법입니다 – 리스트가 만들어지지 않고, 항목은 소비자가 요청할 때만 계산되며, 원한다면 함수가 항목을 영원히 내보낼 수도 있습니다.

2.31.3. 지연 파이프라인

제너레이터는 잘 조합됩니다. 한 제너레이터의 출력이 다른 제너레이터의 입력이 될 수 있습니다:

def numbers():
    i = 0
    while True:
        yield i
        i += 1

def squares(source):
    for x in source:
        yield x * x

pipeline = squares(numbers())

for v in pipeline:
    if v > 100:
        break
    print(v)

값은 파이프라인을 한 번에 하나씩 흘러갑니다 – 중간 리스트도 없고, numbers 에 내장된 상한도 없으며, 소비자(for v in pipeline)가 언제 멈출지 결정합니다.

왼쪽에서 오른쪽으로 세 개의 상자: numbers(), squares(source), 그리고 for-v-in-pipeline 소비자. 그 아래에 세 번의 사이클이 그려져 있습니다. 각 사이클에서 소비자는 squares 로 왼쪽으로 끌어오기 요청을 보내고, squares 는 numbers 로 왼쪽으로 끌어오기 요청을 보냅니다; numbers 는 값을 오른쪽으로 squares 에 내보내고, squares 는 그 제곱값을 오른쪽으로 소비자에게 내보냅니다.

소비자 쪽의 각 next() 는 체인을 따라 한 번의 끌어오기를 일으킵니다; 값은 무언가가 요청할 때만 존재합니다.

2.31.3.1. yield from

다른 이터러블에서 항목을 끌어와 하나씩 yield 하는 루프는 충분히 흔해서 Python이 단축 표현을 제공합니다. yield from iter 식은 마치 제너레이터에 for x in iter: yield x 루프가 인라인으로 들어있는 것처럼, 이터러블이 생성하는 각 값을 순서대로 내보냅니다:

def chain(*sources):
    for source in sources:
        yield from source

for v in chain([1, 2, 3], (4, 5), "abc"):
    print(v)

출력:

1
2
3
4
5
a
b
c

yield from 은 명시적인 for 루프와 정확히 동등하며, 단지 더 짧을 뿐입니다. 또한 내부 이터러블의 StopIteration 을 외부 제너레이터로 깔끔하게 전파합니다 – 여러 제너레이터를 끝에서 끝으로 연결할 때 유용합니다.

2.31.3.2. yield 가 끝났을 때

제너레이터 함수의 끝을 넘어가거나(또는 명시적인 return 에 도달하면) 자동으로 StopIteration 이 발생합니다. 직접 발생시킬 필요는 없으며, 둘러싼 for 루프가 이를 감지하고 종료합니다.

생성하는 코드가 자연스럽게 몇 개의 yield 지점을 가진 루프로 작성될 때는 제너레이터를 사용하고, 전체 시퀀스가 정말로 메모리에 필요할 때는 단순한 리스트 컴프리헨션을 사용하세요.