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
A for loop calls __iter__ once to get an iterator, then calls __next__ repeatedly until StopIteration ends the loop.

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)는 재개 사이에 보존됩니다.

Sequence diagram with two lifelines (caller and generator body). The caller calls count_up_to(3), which creates a generator without running the body. Each subsequent next() runs the body until the next yield, returns the yielded value, and pauses. The fourth next() falls off the end and raises StopIteration. The variable i is preserved across pauses.

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)가 언제 멈출지 결정합니다.

Three boxes left-to-right: numbers(), squares(source), and the for-v-in-pipeline consumer. Three cycles are drawn beneath. In each cycle, the consumer sends a pull request leftward to squares, which sends a pull leftward to numbers; numbers yields a value rightward to squares, which yields its squared value rightward to the consumer.

소비자 쪽의 각 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 지점을 가진 루프로 작성될 때는 제너레이터를 사용하고, 전체 시퀀스가 정말로 메모리에 필요할 때는 단순한 리스트 컴프리헨션을 사용하세요.