Capítulo 34, Intermediário
Iteradores e geradores
Um gerador produz valores sob demanda, um de cada vez. É a ferramenta que permite processar mais dados do que cabe na memória.
Código deste capítulo: intermediario/cap34_geradores.py
Iterável e iterador
Um iterável é qualquer objeto que pode ser percorrido por um for (lista, string, arquivo). Um iterador é o objeto que entrega os itens um a um, com next(), e levanta StopIteration quando acaba. O for faz exatamente isso por baixo:
lista = [1, 2, 3]
iterador = iter(lista)
print(next(iterador), next(iterador), next(iterador))
try:
next(iterador)
except StopIteration:
print("acabou")
1 2 3
acabou
Você pode criar o seu próprio iterador com uma classe que tenha __iter__ e __next__:
class Contagem:
def __init__(self, limite):
self.limite = limite
self.atual = 0
def __iter__(self):
return self
def __next__(self):
if self.atual >= self.limite:
raise StopIteration
self.atual += 1
return self.atual
print(list(Contagem(4)))
[1, 2, 3, 4]
Funções geradoras com yield
Escrever uma classe só para isso é trabalhoso. Uma função com yield vira um gerador automaticamente. Ela pausa a cada yield e retoma de onde parou:
def contagem(limite):
atual = 1
while atual <= limite:
yield atual
atual += 1
print(list(contagem(4)))
gerador = contagem(2)
print(next(gerador), next(gerador))
[1, 2, 3, 4]
1 2
Preguiça é economia
Um gerador não guarda os itens. Ele guarda só o ponto em que parou. Por isso gerar cem mil números ocupa quase nada, enquanto a lista equivalente ocupa megabytes:
import sys
lista = [n for n in range(100_000)]
gerador = (n for n in range(100_000))
print(sys.getsizeof(gerador) < sys.getsizeof(lista))
print(sum(gerador))
True
4999950000
Como o gerador não tem fim definido, ele pode até ser infinito, desde que quem consome pare no momento certo:
from itertools import islice
def fibonacci():
a, b = 0, 1
while True:
yield a
a, b = b, a + b
print(list(islice(fibonacci(), 10)))
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
Pipelines
Geradores se encadeiam: cada etapa recebe o gerador da anterior e processa um item por vez. Eu uso esse padrão para ler arquivos grandes: a linha entra, é filtrada, transformada, e só então a próxima é lida:
def sem_vazias(linhas):
for linha in linhas:
if linha.strip():
yield linha
def numerar(linhas):
for numero, linha in enumerate(linhas, start=1):
yield f"{numero}: {linha}"
texto = ["primeira", "", "segunda", " ", "terceira"]
print(list(numerar(sem_vazias(texto))))
['1: primeira', '2: segunda', '3: terceira']
Um gerador só pode ser percorrido uma vez
Depois de esgotado, o gerador não volta. Esta é a pegadinha mais comum:
g = (n for n in range(3))
print(list(g), list(g))
[0, 1, 2] []
Se você precisa percorrer duas vezes, guarde em uma lista ou recrie o gerador.
Em lotes
Desde o Python 3.12, itertools.batched divide qualquer iterável em grupos de tamanho fixo, o que é útil para enviar dados a uma API em blocos:
from itertools import batched
print(list(batched("abcdefg", 3)))
[('a', 'b', 'c'), ('d', 'e', 'f'), ('g',)]
Exercício 1
Dividir em lotes
Escreva o gerador lotes(iteravel, tamanho) que devolva listas de até tamanho itens, usando islice.