Geradores e iteradores em Python: lazy evaluation na prática
1. Fundamentos de Iteradores em Python
O protocolo de iteração em Python é a base sobre a qual todo o sistema de loops e compreensões de coleções é construído. Um objeto é considerado iterável quando implementa o método __iter__(), que retorna um iterador. O iterador, por sua vez, implementa __next__(), que retorna o próximo elemento ou levanta StopIteration quando não há mais elementos.
Vamos construir um iterador personalizado do zero:
class ContadorInfinito:
def __init__(self, inicio=0):
self.valor = inicio
def __iter__(self):
return self
def __next__(self):
valor_atual = self.valor
self.valor += 1
return valor_atual
# Uso
contador = ContadorInfinito(10)
print(next(contador)) # 10
print(next(contador)) # 11
A diferença fundamental entre objetos iteráveis e iteradores é sutil mas crucial. Uma lista é iterável, mas não é um iterador:
lista = [1, 2, 3]
print(hasattr(lista, '__next__')) # False
print(hasattr(lista, '__iter__')) # True
iterador = iter(lista)
print(hasattr(iterador, '__next__')) # True
print(next(iterador)) # 1
2. Geradores: Sintaxe Simplificada para Lazy Evaluation
Geradores são a forma mais elegante de criar iteradores em Python. A palavra-chave yield transforma uma função comum em uma função geradora, que retorna um objeto gerador. Cada chamada a next() executa a função até o próximo yield, suspendendo seu estado entre chamadas.
def gerar_numeros_pares(limite):
n = 0
while n < limite:
yield n
n += 2
pares = gerar_numeros_pares(10)
print(list(pares)) # [0, 2, 4, 6, 8]
A economia de memória é dramática para grandes volumes:
import sys
# Lista: carrega tudo na memória
lista_grande = [x for x in range(10_000_000)]
print(sys.getsizeof(lista_grande)) # ~80 MB
# Gerador: praticamente zero de memória
gerador_grande = (x for x in range(10_000_000))
print(sys.getsizeof(gerador_grande)) # ~112 bytes
3. Expressões Geradoras vs List Comprehensions
As expressões geradoras usam parênteses em vez de colchetes e produzem valores sob demanda:
# List comprehension: materializa todos os valores
quadrados_lista = [x**2 for x in range(10)]
print(type(quadrados_lista)) # <class 'list'>
# Generator expression: lazy evaluation
quadrados_gerador = (x**2 for x in range(10))
print(type(quadrados_gerador)) # <class 'generator'>
Para processar arquivos enormes, a diferença é crítica:
def processar_arquivo_grande(caminho):
with open(caminho, 'r') as arquivo:
linhas = (linha.strip() for linha in arquivo)
linhas_validas = (linha for linha in linhas if linha.startswith('ERROR'))
for linha in linhas_validas:
yield processar_log(linha)
# Uso: nunca carrega o arquivo inteiro na memória
for resultado in processar_arquivo_grande('servidor.log'):
print(resultado)
4. Padrões Avançados com Geradores
Geradores podem ser encadeados para criar pipelines de processamento elegantes:
numeros = (x for x in range(100))
pares = (x for x in numeros if x % 2 == 0)
quadrados = (x**2 for x in pares)
primeiros_5 = list(quadrados)[:5]
print(primeiros_5) # [0, 4, 16, 36, 64]
O yield from permite delegar a subgeradores:
def gerar_sequencia():
yield from range(3)
yield from 'ABC'
print(list(gerar_sequencia())) # [0, 1, 2, 'A', 'B', 'C']
Para geradores infinitos, itertools.islice é essencial:
from itertools import islice
def fibonacci_infinito():
a, b = 0, 1
while True:
yield a
a, b = b, a + b
primeiros_10 = list(islice(fibonacci_infinito(), 10))
print(primeiros_10) # [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
5. Corrotinas e Envio de Dados com send()
O método send() permite comunicação bidirecional com geradores:
def acumulador():
total = 0
while True:
valor = yield total
if valor is not None:
total += valor
acc = acumulador()
next(acc) # Inicializa o gerador
print(acc.send(10)) # 10
print(acc.send(20)) # 30
print(acc.send(5)) # 35
6. Lazy Evaluation em Processamento de Dados Reais
Um pipeline ETL completo com geradores:
import csv
from itertools import islice
def ler_csv(caminho):
with open(caminho, 'r') as arquivo:
leitor = csv.DictReader(arquivo)
for linha in leitor:
yield linha
def filtrar_por_data(linhas, ano):
for linha in linhas:
if linha['data'].startswith(str(ano)):
yield linha
def transformar_para_json(linhas):
for linha in linhas:
yield {
'id': int(linha['id']),
'valor': float(linha['valor']),
'data': linha['data']
}
# Pipeline lazy
dados = ler_csv('vendas.csv')
dados_2023 = filtrar_por_data(dados, 2023)
dados_transformados = transformar_para_json(dados_2023)
primeiros_100 = list(islice(dados_transformados, 100))
7. Performance e Armadilhas Comuns
Geradores não são sempre a melhor escolha:
# Problema: acesso repetido esgota o gerador
def dados():
for i in range(5):
yield i
gen = dados()
print(sum(gen)) # 10
print(sum(gen)) # 0 (gerador esgotado!)
# Solução: converter para lista se precisar reutilizar
gen = list(dados())
print(sum(gen)) # 10
print(sum(gen)) # 10
8. Integração com Bibliotecas e Frameworks
Geradores são excelentes para streaming em web scraping:
import requests
from bs4 import BeautifulSoup
def scrape_paginas(urls):
for url in urls:
resposta = requests.get(url)
soup = BeautifulSoup(resposta.text, 'html.parser')
yield soup.title.text
# Processa uma página por vez, sem carregar todas
for titulo in scrape_paginas(['https://exemplo.com/pagina1', 'https://exemplo.com/pagina2']):
print(titulo)
Em frameworks web como Flask, geradores permitem streaming de respostas:
from flask import Flask, Response
import time
app = Flask(__name__)
def gerar_dados():
for i in range(10):
yield f"Dado {i}\n"
time.sleep(1)
@app.route('/stream')
def stream():
return Response(gerar_dados(), mimetype='text/plain')
Referências
- Documentação Oficial: Geradores Python — Guia completo sobre geradores na documentação oficial do Python, com exemplos de yield e expressões geradoras.
- PEP 255: Simple Generators — Proposta original que introduziu geradores no Python, explicando motivações e especificações técnicas.
- Real Python: Introduction to Python Generators — Tutorial prático cobrindo desde conceitos básicos até padrões avançados com geradores.
- Python Module of the Week: itertools — Referência completa sobre o módulo itertools, essencial para trabalhar com geradores e lazy evaluation.
- GeeksforGeeks: Generators in Python — Artigo detalhado com exemplos comparativos de performance entre listas e geradores.