Generator e yield em Python: processando dados grandes sem gastar RAM
1. Visão geral
Ao processar arquivos de log de vários GB ou fluxos de milhões de registros de um banco de dados, escrever código “carrega tudo em uma list e depois processa” vai fazer o programa consumir RAM descontroladamente e talvez travar. Este artigo ajuda você a entender generator e yield, como funcionam, comparar com listas normais e aplicá-los a problemas reais: ler arquivos grandes, construir pipelines de processamento de dados. Depois de ler, você será capaz de escrever generators corretamente e saberá quando evitá-los.
2. O problema com a abordagem tradicional
Suponha que você precise ler um arquivo de log de 5GB e contar as linhas que contêm a palavra “ERROR”:
def count_errors_bad(filepath: str) -> int:
with open(filepath, "r") as f:
lines = f.readlines() # carrega o ARQUIVO INTEIRO na memória RAM
return sum(1 for line in lines if "ERROR" in line)
f.readlines() lê todo o conteúdo do arquivo para uma lista de strings na memória. Com um arquivo de 5GB, Python vai alocar ~5GB+ de RAM (mais o overhead do objeto) antes de começar a contar. Se a máquina tiver apenas 4GB de RAM, o programa vai gerar um MemoryError ou travar o sistema por causa do swap.
O problema central: você só precisa processar uma linha por vez, mas o código força a máquina a manter todas as linhas ao mesmo tempo.
3. O que é um generator?
Um generator é um tipo especial de iterable: ele gera valores sob demanda (lazy evaluation) em vez de calcular e armazenar tudo antecipadamente. Em vez de retornar uma lista completa, o generator retorna um valor por vez, e só calcula o próximo valor quando solicitado.
def count_errors_good(filepath: str) -> int:
with open(filepath, "r") as f:
return sum(1 for line in f if "ERROR" in line)
Aqui, f (o objeto de arquivo) em si já é um iterador — ele lê uma linha por vez quando o loop solicita, sem carregar o arquivo inteiro na RAM. A memória usada é apenas de uma linha por vez, independentemente de o arquivo ter 5GB ou 50GB.
4. A palavra-chave yield: como criar uma generator function
Você pode escrever seu próprio generator usando yield dentro de uma função. Quando uma função contém yield, Python a transforma automaticamente em uma generator function — chamá-la não executa o código imediatamente, em vez disso, retorna um objeto generator.
def read_large_file(filepath: str):
with open(filepath, "r") as f:
for line in f:
yield line.strip()
yield line.strip()pausa a função naquele ponto, retornando o valorline.strip()para o chamador.- A próxima chamada de
next()faz a função continuar do ponto em que parou, e não recomeça do início. - O estado da função (variáveis locais, posição do ponteiro do arquivo) é preservado entre as chamadas de
yield.
Experimente chamar:
gen = read_large_file("access.log")
print(type(gen))
print(next(gen))
print(next(gen))
<class 'generator'>
127.0.0.1 - GET /index.html 200
127.0.0.1 - GET /style.css 200
Nota: a linha gen = read_large_file(...) não abre o arquivo, não lê nada. O código dentro só é executado quando você chama next(gen) pela primeira vez.
5. Comparando RAM: generator vs list
Para ver a diferença claramente, use o módulo sys para medir o tamanho do objeto:
import sys
def squares_list(n: int) -> list[int]:
return [i * i for i in range(n)]
def squares_gen(n: int):
for i in range(n):
yield i * i
big_list = squares_list(1_000_000)
big_gen = squares_gen(1_000_000)
print(f"List size: {sys.getsizeof(big_list):,} bytes")
print(f"Generator size: {sys.getsizeof(big_gen):,} bytes")
List size: 8,448,728 bytes
Generator size: 208 bytes
Uma lista contendo 1 milhão de elementos ocupa cerca de 8.5MB, enquanto o generator é apenas um pequeno objeto de 208 bytes — porque não armazena nenhum valor, apenas mantém o estado para calcular o próximo valor quando necessário.
Nota: Um generator só pode ser iterado uma vez. Depois de iterar sobre ele completamente, chamar
next()novamente vai lançarStopIteration. Se você precisar iterar várias vezes, deve criar um novo generator chamando a função novamente.
6. Generator expression: sintaxe mais concisa
Semelhante a uma list comprehension, mas usando parênteses em vez de colchetes, você tem uma expressão geradora — sem precisar definir uma função separada:
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
even_squares_gen = (n * n for n in numbers if n % 2 == 0)
print(sum(even_squares_gen))
220
Os parênteses () em vez de [] fazem Python criar um generator em vez de uma lista imediatamente. Isso é conveniente quando você só precisa usar os valores uma vez, por exemplo, passá-los diretamente para sum(), max() ou any().
7. Construindo um pipeline de processamento de dados com generators
O ponto forte dos generators é poder encadear várias etapas de processamento sem criar nenhuma lista intermediária. Suponha que você precise: ler arquivo CSV de pedidos → filtrar aqueles com status = “PAID” → calcular a soma de amount.
import csv
def read_orders(filepath: str):
with open(filepath, newline="") as f:
reader = csv.DictReader(f)
for row in reader:
yield row
def filter_paid(orders):
for order in orders:
if order["status"] == "PAID":
yield order
def extract_amount(orders):
for order in orders:
yield float(order["amount"])
def total_paid_amount(filepath: str) -> float:
orders = read_orders(filepath)
paid_orders = filter_paid(orders)
amounts = extract_amount(paid_orders)
return sum(amounts)
Cada função read_orders, filter_paid, extract_amount é um generator — elas não processam todos os dados imediatamente, apenas as ‘conectam’ em um pipeline. Quando sum(amounts) chama next(), o valor é puxado através de cada etapa: lê 1 linha CSV → verifica o status → converte para float → acumula. Em qualquer momento, apenas um registro existe na memória, quer o arquivo CSV tenha 10 linhas ou 10 milhões de linhas.
7.1. Testando o pipeline com pytest
import pytest
from pathlib import Path
@pytest.fixture
def orders_csv(tmp_path: Path) -> str:
content = (
"order_id,amount,status\n"
"1,100.0,PAID\n"
"2,50.0,PENDING\n"
"3,200.0,PAID\n"
)
file_path = tmp_path / "orders.csv"
file_path.write_text(content)
return str(file_path)
def test_total_paid_amount(orders_csv):
result = total_paid_amount(orders_csv)
assert result == pytest.approx(300.0)
tmp_path é uma fixture nativa do pytest que cria um diretório temporário separado para cada teste. Este teste cria um arquivo CSV fictício com 3 pedidos, apenas 2 pedidos PAID, e verifica se o total é igual a 300.0.
8. yield from: delegando a outro generator
Quando um generator precisa chamar e incorporar resultados de outro generator, yield from ajuda a tornar o código mais conciso em vez de escrever um loop manual.
def read_file_lines(filepath: str):
with open(filepath) as f:
for line in f:
yield line.strip()
def read_multiple_files(filepaths: list[str]):
for filepath in filepaths:
yield from read_file_lines(filepath)
yield from read_file_lines(filepath) é equivalente a escrever for line in read_file_lines(filepath): yield line, mas mais conciso e lida corretamente com o caso de um generator aninhado ter um valor de return. A função read_multiple_files permite iterar sobre vários arquivos de log como se fossem um fluxo contínuo, sem precisar mesclar listas.
9. Enviando valores para um generator com send()
Um generator não apenas produz valores para fora — você também pode enviar valores para dentro dele usando send(), transformando-o em uma corrotina simples.
def running_average():
total = 0.0
count = 0
average = None
while True:
value = yield average
total += value
count += 1
average = total / count
avg_gen = running_average()
next(avg_gen) # inicializar o generator, execute até o primeiro yield
print(avg_gen.send(10)) # envie 10, average = 10/1
print(avg_gen.send(20)) # envie 20, average = 30/2
print(avg_gen.send(30)) # envie 30, average = 60/3
10.0
15.0
20.0
A linha value = yield average faz duas coisas: retorna average para fora e ao mesmo tempo aguarda um valor enviado via send() para atribuir a value. Você deve chamar next(avg_gen) uma vez primeiro para levar o generator ao primeiro ponto de yield — se você chamar send() logo no início quando o generator ainda não foi executado, Python vai lançar um TypeError.
Nota: Usar
send()torna o código mais difícil de ler do que generators comuns. Na prática, frameworks comoasynciojá abstraem esse mecanismo; você raramente precisa escreversend()por conta própria, a menos que esteja criando sua própria biblioteca.
10. Quando NÃO usar generators
Um generator nem sempre é melhor que uma lista:
- Precisa de acesso aleatório ou iterar várias vezes: generators não suportam
gen[5]ou iteração de novo do início. Se você precisa delen(), slicing ou iterar os dados 2-3 vezes, lista é mais apropriada. - Dados pequenos, sem preocupações com RAM: com alguns milhares de elementos, o overhead do gerenciamento de estado do generator às vezes é mais lento que uma list comprehension, porque listas são otimizadas em nível C.
- Precisa de paralelização ou distribuir o trabalho para múltiplos workers: listas são fáceis de dividir em pedaços (chunks) para enviar para multiprocessing; generators precisam ser ‘materializados’ parcialmente antes de serem divididos.
- Debugging é mais difícil: imprimir uma lista mostra imediatamente todos os valores; imprimir um generator apenas mostra
<generator object ... at 0x...>. Para ver os valores você precisa iterar (e quando termina, o generator está ‘exaurido’ e não pode ser usado novamente).
Se você não tem certeza, a regra simples é: use lista quando os dados cabem na RAM e você precisa usá-los várias vezes; use generator quando os dados são grandes, processá-los sequencialmente uma única vez ou construir um pipeline de streaming.
11. Conclusão
Generators e yield resolvem corretamente o problema: processar dados grandes sem precisar carregá-los integralmente na RAM, graças ao mecanismo de avaliação preguiçosa — calculando valores apenas quando solicitados. Você viu como escrever uma generator function, expressões geradores, conectar múltiplos generators em um pipeline usando yield from, e como enviar dados de volta para um generator usando send(). Lembre-se da compensação: generators economizam memória, mas em troca você perde a capacidade de iterar novamente, acesso aleatório e às vezes o código é mais difícil de ler. Escolha a ferramenta certa para o trabalho certo — não comece a usar generators em todos os lugares apenas porque “economiza RAM”.