Generators e yield em Python
Aprenda generators e a palavra-chave yield em Python com exemplos práticos sobre funções geradoras, expressões, send() e casos de uso reais.
Um generator é um tipo especial de iterador que produz valores um de cada vez, sob demanda, em vez de calculá-los todos de uma vez. Os generators são definidos usando a sintaxe comum de função com yield no lugar de return. Eles são a solução idiomática em Python para sequências grandes ou infinitas, onde construir uma lista completa desperdiçaria memória ou tempo.
Este capítulo aborda a palavra-chave yield, funções geradoras versus listas, expressões geradoras, como enviar valores para um generator, encadeamento de generators e padrões do mundo real.
O que é um Generator?
Quando Python chama uma função regular, ela executa o corpo até o fim e retorna um valor. Quando Python chama uma função geradora, ela não executa o corpo de imediato — retorna um objeto generator. Cada vez que você chama next() nesse objeto, a execução é retomada de onde parou (na instrução yield), roda até o próximo yield e é suspensa novamente.
def count_up(start, stop):
while start <= stop:
yield start # pause here, emit the value
start += 1
gen = count_up(1, 3)
print(next(gen)) # 1
print(next(gen)) # 2
print(next(gen)) # 3
# next(gen) would now raise StopIterationMecânica essencial:
- O corpo da função não é executado até a primeira chamada de
next(). - Variáveis locais e o ponteiro de instrução são preservados entre as chamadas.
- Quando o corpo da função termina (ou encontra um
returnsimples), Python levantaStopIterationautomaticamente. - Um laço
forchamanext()por você e para corretamente noStopIteration.
A Palavra-chave yield
yield é a única sintaxe que distingue uma função geradora de uma função comum. Você pode usar yield em qualquer lugar onde return pudesse aparecer, inclusive dentro de laços, condicionais e blocos try/except.
yield vs return
return | yield | |
|---|---|---|
| Tipo de função | Regular | Geradora |
| Execução após chamada | Roda até o fim | Pausa no yield |
| Estado entre chamadas | Descartado | Preservado |
| Múltiplos valores | Um (ou uma tupla) | Um por yield, sequencialmente |
| Memória para grandes dados | Armazena todos os valores | Armazena um valor por vez |
yield Suspende, Não Termina
def three_things():
print("about to yield first")
yield "first"
print("about to yield second")
yield "second"
print("about to yield third")
yield "third"
print("generator exhausted")
for item in three_things():
print("got:", item)Saída:
about to yield first
got: first
about to yield second
got: second
about to yield third
got: third
generator exhaustedObserve as instruções print entre os yields — código normal é executado entre cada suspensão.
Funções Geradoras vs Listas
Considere gerar os primeiros n quadrados. Usando uma lista:
def squares_list(n):
result = []
for i in range(1, n + 1):
result.append(i * i)
return result
print(squares_list(5)) # [1, 4, 9, 16, 25]Usando um generator:
def squares_gen(n):
for i in range(1, n + 1):
yield i * i
gen = squares_gen(5)
print(list(gen)) # [1, 4, 9, 16, 25]Ambos produzem os mesmos valores, mas a versão com generator:
- Usa memória O(1) independentemente de
n(a versão com lista usa O(n)) - Começa a produzir valores imediatamente, sem esperar construir toda a coleção
- Pode representar sequências infinitas (uma lista não pode)
Quando Escolher um Generator
Use um generator quando:
- Você precisa iterar pelos valores apenas uma vez.
- A sequência é grande o suficiente para que manter tudo na memória seja relevante.
- Você está construindo um pipeline de dados (um generator alimenta outro).
- A sequência é potencialmente infinita (por exemplo, lendo linhas de log de um arquivo ao vivo).
Use uma lista quando:
- Você precisa de acesso aleatório por índice.
- Você precisa iterar a mesma sequência várias vezes.
- Você precisa de
len(), fatiamento ou ordenação no lugar.
Expressões Geradoras
Uma expressão geradora está para generators assim como uma compreensão de lista está para listas. A sintaxe é idêntica, exceto pelo uso de parênteses em vez de colchetes:
# List comprehension — builds the full list immediately
squares_list = [x * x for x in range(1, 6)]
# Generator expression — lazy, produces one value at a time
squares_gen = (x * x for x in range(1, 6))
print(type(squares_list)) # <class 'list'>
print(type(squares_gen)) # <class 'generator'>
print(list(squares_gen)) # [1, 4, 9, 16, 25]Expressões geradoras são mais úteis quando passadas diretamente a uma função que consome um iterável:
total = sum(x * x for x in range(1, 101)) # sum of squares 1..100
print(total) # 338350Não são necessários parênteses extras quando a expressão geradora é o único argumento de uma chamada de função.
Filtrando com Expressões Geradoras
evens = (x for x in range(20) if x % 2 == 0)
print(list(evens)) # [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]Generators Infinitos
Como um generator produz valores de forma preguiçosa (lazy), ele pode representar uma sequência sem fim. O exemplo clássico é um contador infinito:
def counter(start=0):
n = start
while True:
yield n
n += 1
gen = counter(10)
print(next(gen)) # 10
print(next(gen)) # 11
print(next(gen)) # 12Para consumir apenas parte de um generator infinito, use itertools.islice ou saia de um laço:
import itertools
gen = counter(1)
first_five = list(itertools.islice(gen, 5))
print(first_five) # [1, 2, 3, 4, 5]Um generator infinito prático — a sequência de Fibonacci:
def fibonacci():
a, b = 0, 1
while True:
yield a
a, b = b, a + b
fib = fibonacci()
print([next(fib) for _ in range(10)])
# [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]yield from — Delegando a um Sub-Generator
yield from permite que um generator delegue a outro iterável, encaminhando cada valor de forma transparente:
def first_part():
yield 1
yield 2
def second_part():
yield 3
yield 4
def combined():
yield from first_part()
yield from second_part()
print(list(combined())) # [1, 2, 3, 4]yield from também funciona com qualquer iterável, não apenas generators:
def flatten(nested):
for sublist in nested:
yield from sublist
data = [[1, 2], [3, 4], [5, 6]]
print(list(flatten(data))) # [1, 2, 3, 4, 5, 6]yield from é mais limpo do que um laço for aninhado sobre o sub-iterável e encaminha corretamente chamadas de send() e throw() ao generator delegado (importante para padrões de coroutine).
Enviando Valores para um Generator
Generators são canais bidirecionais. O método .send(value) retoma o generator e passa um valor de volta como resultado da expressão yield:
def accumulator():
total = 0
while True:
value = yield total # yield sends total out; receives value in
if value is None:
break
total += value
gen = accumulator()
next(gen) # prime the generator (advance to first yield)
print(gen.send(10)) # 10
print(gen.send(20)) # 30
print(gen.send(5)) # 35Regras para .send():
- Você deve chamar
next(gen)(ougen.send(None)) uma vez para avançar o generator até o primeiroyieldantes de enviar um valor diferente deNone. send(None)é equivalente anext().- O valor enviado torna-se o resultado da expressão
yieldno lado esquerdo.
Estado do Generator e Esgotamento
Um objeto generator tem um ciclo de vida com quatro estados:
| Estado | Descrição |
|---|---|
| Criado | Função geradora chamada, corpo ainda não iniciado |
| Em execução | Executando no momento (dentro de uma chamada next() ou send()) |
| Suspenso | Pausado em um yield; será retomado no próximo next() |
| Fechado | Corpo finalizado ou .close() chamado; levanta StopIteration |
Uma vez esgotado, reiterar um generator não produz nada:
gen = (x for x in range(3))
print(list(gen)) # [0, 1, 2]
print(list(gen)) # [] — already exhaustedSe você precisar iterar a saída de um generator mais de uma vez, converta-o em uma lista primeiro ou recrie o generator.
return Dentro de um Generator
Uma instrução return dentro de um generator encerra a iteração de forma limpa. O valor passado ao return torna-se o atributo value da exceção StopIteration (raramente usado diretamente, mas importante para a delegação com yield from):
def limited():
yield 1
yield 2
return "done" # StopIteration.value = "done"
gen = limited()
print(next(gen)) # 1
print(next(gen)) # 2
try:
next(gen)
except StopIteration as e:
print(e.value) # donePadrões do Mundo Real
Lendo um Arquivo Grande Linha por Linha
def read_lines(filepath):
with open(filepath) as f:
for line in f:
yield line.rstrip("\n")
# Memory usage stays constant regardless of file size
for line in read_lines("/etc/hosts"):
if line.startswith("#"):
continue
print(line)Construindo um Pipeline de Dados
Generators se compõem naturalmente em pipelines onde cada etapa transforma o fluxo:
def integers(n):
for i in range(1, n + 1):
yield i
def only_even(nums):
for n in nums:
if n % 2 == 0:
yield n
def squared(nums):
for n in nums:
yield n * n
# Compose: even squares from 1..20
pipeline = squared(only_even(integers(20)))
print(list(pipeline))
# [4, 16, 36, 64, 100, 144, 196, 256, 324, 400]Cada etapa é preguiçosa (lazy) — os valores fluem pelo pipeline um de cada vez, sem construir listas intermediárias.
Dividindo um Iterável em Lotes
def chunks(iterable, size):
chunk = []
for item in iterable:
chunk.append(item)
if len(chunk) == size:
yield chunk
chunk = []
if chunk:
yield chunk
data = list(range(10))
for batch in chunks(data, 3):
print(batch)
# [0, 1, 2]
# [3, 4, 5]
# [6, 7, 8]
# [9]Generators vs Iteradores vs Compreensões
| Recurso | Classe iteradora | Função geradora | Expressão geradora |
|---|---|---|---|
| Sintaxe | Classe com __iter__/__next__ | def + yield | (expr for x in ...) |
| Verbosidade | Alta | Baixa | Muito baixa |
| Gerenciamento de estado | Manual | Automático | Automático |
| Lógica com múltiplas instruções | Sim | Sim | Não (expressão única) |
| Sequências infinitas | Sim | Sim | Sim |
| Legibilidade para lógica complexa | Sim | Sim | Não |
Para qualquer coisa além de uma transformação ou filtro simples, uma função geradora é mais legível do que uma expressão geradora. Para iteração complexa com estado, uma função geradora é quase sempre preferível a escrever uma classe iteradora completa — veja Iteradores Python para a abordagem baseada em classes.
Expressões geradoras combinam naturalmente com compreensões de lista e compreensões de dicionário/conjunto. Decoradores também podem envolver funções geradoras para adicionar comportamento de cache ou rastreamento.