2.31. Iteradores e geradores

O laço for vem fazendo mais trabalho do que aparenta. Esta página cobre o protocolo de iterador sobre o qual ele opera e a palavra-chave yield, que permite construir seus próprios iteradores.

2.31.1. O protocolo de iterador

Todo objeto que pode ser percorrido em um laço implementa dois métodos:

  • __iter__() – retorna um iterador sobre os itens do objeto.

  • __next__() – no iterador, retorna o próximo item ou levanta StopIteration quando não há mais nenhum.

A função embutida iter() chama __iter__; next() chama __next__. Percorra uma lista manualmente:

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 é açúcar sintático para “chamar __iter__ uma vez, depois iterar sobre __next__ até StopIteration.”

O que for x in items: realmente faz:

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

Toda lista, tupla, string, dict, set, objeto de arquivo e gerador já implementa __iter__ e __next__ – e é por isso que todos funcionam com for.

2.31.2. yield e funções geradoras

Uma função que contém uma instrução yield é uma função geradora. Chamá-la não executa o corpo; ela retorna um objeto gerador (um iterador) que executa o corpo um yield por vez:

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

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

Saída:

0
1
2

Cada chamada a next() retoma a função até o próximo yield, entrega esse valor ao chamador e pausa ali. O estado local (i neste caso) é preservado entre as retomadas.

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() executa o corpo até o próximo yield, devolve o valor e pausa. O estado local sobrevive à pausa.

Geradores são a maneira mais fácil de produzir uma sequência preguiçosamente – nenhuma lista é construída, os itens são calculados apenas quando o consumidor os solicita, e a função pode produzir itens indefinidamente, se quiser.

2.31.3. Pipelines preguiçosos

Geradores se compõem bem. A saída de um gerador pode alimentar outro:

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)

Os valores fluem pelo pipeline um de cada vez – sem lista intermediária, sem limite superior embutido em numbers, e o consumidor (for v in pipeline) decide quando parar.

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.

Cada next() no consumidor dispara um puxar através da cadeia; os valores só existem quando algo os solicita.

2.31.3.1. yield from

Um laço que puxa itens de outro iterável e produz cada um é comum o suficiente para que o Python ofereça um atalho. A expressão yield from iter produz cada valor que o iterável gera, em ordem – como se o gerador tivesse um laço for x in iter: yield x embutido:

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

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

Saída:

1
2
3
4
5
a
b
c

yield from é exatamente equivalente ao laço for explícito, apenas mais curto, e propaga StopIteration do iterável interno para o gerador externo de forma limpa – útil ao encadear vários geradores de ponta a ponta.

2.31.3.2. Quando o yield se esgota

Chegar ao fim de uma função geradora (ou atingir um return explícito) levanta StopIteration automaticamente. Não é necessário levantá-la manualmente; o laço for ao redor a percebe e encerra.

Use geradores quando o código produtor for naturalmente escrito como um laço com alguns pontos de yield; use uma compreensão de lista simples quando você realmente precisar de toda a sequência na memória.