Generator expressions: lazy evaluation
1. O que são Generator Expressions e o conceito de Lazy Evaluation
Generator expressions são uma forma concisa e eficiente de criar iteradores em Python. Sua sintaxe básica utiliza parênteses: (expr for item in iterable). A diferença fundamental para as list comprehensions está nos delimitadores: enquanto list comprehensions usam colchetes [], generator expressions usam parênteses ().
# List comprehension (eager evaluation)
quadrados_lista = [x**2 for x in range(10)]
# Generator expression (lazy evaluation)
quadrados_gerador = (x**2 for x in range(10))
print(type(quadrados_lista)) # <class 'list'>
print(type(quadrados_gerador)) # <class 'generator'>
O conceito de lazy evaluation (avaliação preguiçosa) significa que os valores são computados sob demanda, apenas quando solicitados, em oposição à eager evaluation (avaliação ansiosa), onde todos os elementos são calculados e armazenados imediatamente na memória.
# Eager: todos os 10 milhões de elementos são calculados e armazenados
numeros_eager = [x for x in range(10_000_000)] # Consome ~80MB de RAM
# Lazy: nenhum elemento é calculado até ser solicitado
numeros_lazy = (x for x in range(10_000_000)) # Consome ~56 bytes
2. Comparação com List Comprehensions: Memória e Performance
A principal vantagem dos generator expressions é a eficiência de memória. Enquanto list comprehensions armazenam todos os elementos na memória, generators produzem um elemento por vez.
import sys
# Comparação de tamanho em memória
lista_grande = [x for x in range(100_000)]
gerador_grande = (x for x in range(100_000))
print(f"List comprehension: {sys.getsizeof(lista_grande)} bytes") # ~824,464 bytes
print(f"Generator expression: {sys.getsizeof(gerador_grande)} bytes") # ~56 bytes
Para operações que processam grandes volumes de dados sem necessidade de armazenamento, generators são dramaticamente mais eficientes:
import time
# Processando 10 milhões de números
def processar_com_lista():
return sum([x**2 for x in range(10_000_000)])
def processar_com_generator():
return sum(x**2 for x in range(10_000_000))
# Generator expression é mais rápido e usa menos memória
inicio = time.time()
resultado_lista = processar_com_lista()
print(f"List comprehension: {time.time() - inicio:.2f}s")
inicio = time.time()
resultado_gerador = processar_com_generator()
print(f"Generator expression: {time.time() - inicio:.2f}s")
3. Sintaxe e Estrutura de Generator Expressions
Generator expressions suportam a mesma estrutura que list comprehensions, incluindo cláusulas if opcionais e loops aninhados:
# Com filtro condicional
pares = (x for x in range(100) if x % 2 == 0)
# Loops aninhados
pares_ordenados = ((x, y) for x in range(3) for y in range(3))
print(list(pares_ordenados)) # [(0,0), (0,1), (0,2), (1,0), ...]
# Combinando com funções built-in
numeros = range(100)
soma = sum(x for x in numeros if x % 3 == 0)
todos_positivos = all(x > 0 for x in [1, 2, 3, 4])
algum_maior_que_10 = any(x > 10 for x in [5, 8, 12, 3])
maximo = max(x**2 for x in range(50))
minimo = min(len(palavra) for palavra in ["python", "java", "c"])
4. Iteração Única e o Protocolo de Iteradores
Generator expressions são iteradores, o que significa que seguem o protocolo de iteração de Python: implementam __iter__() e __next__(). Uma característica crucial é que eles só podem ser iterados uma vez.
gen = (x for x in range(5))
# Primeira iteração funciona normalmente
for valor in gen:
print(valor, end=" ") # 0 1 2 3 4
print()
# Segunda iteração - gerador já está exaurido
for valor in gen:
print(valor, end=" ") # Nada é impresso
# Verificando o estado do gerador
print(list(gen)) # []
O protocolo de iteração implícito:
gen = (x**2 for x in range(3))
# Equivalente ao que acontece internamente
iterador = gen.__iter__()
print(iterador.__next__()) # 0
print(iterador.__next__()) # 1
print(iterador.__next__()) # 4
# print(iterador.__next__()) # StopIteration
5. Casos de Uso Práticos
Processamento de arquivos grandes linha a linha:
# Lendo um arquivo de 10GB sem carregá-lo inteiramente na memória
def processar_log(nome_arquivo):
with open(nome_arquivo, 'r') as arquivo:
# Generator expression processa uma linha por vez
linhas_erro = (linha for linha in arquivo if "ERROR" in linha)
for linha in linhas_erro:
# Processa cada linha de erro individualmente
print(linha.strip())
# Uso: processar_log("arquivo_gigante.log")
Pipeline de transformações sem cópia intermediária:
dados = range(1000)
# Pipeline lazy - nenhuma lista intermediária é criada
pipeline = (
x * 2
for x in dados
if x % 2 == 0
)
pipeline_final = (
x / 3
for x in pipeline
if x > 10
)
# Apenas quando iteramos, os cálculos são realizados
resultado = list(pipeline_final)[:5]
print(resultado)
Uso com funções que aceitam iteráveis:
# Combinando com filter(), map(), zip()
nomes = ["Ana", "Bruno", "Carla", "Daniel"]
idades = [25, 32, 28, 35]
# Generator expression com zip
dados_filtrados = (
(nome, idade)
for nome, idade in zip(nomes, idades)
if idade > 28
)
print(list(dados_filtrados)) # [('Bruno', 32), ('Daniel', 35)]
6. Limitações e Armadilhas Comuns
Impossibilidade de indexação e slicing:
gen = (x for x in range(10))
# gen[0] # TypeError: 'generator' object is not subscriptable
# gen[2:5] # TypeError: 'generator' object is not subscriptable
Efeitos colaterais com variáveis mutáveis:
# Armadilha comum com closures
funcs = [(lambda: x) for x in range(5)]
print([f() for f in funcs]) # [4, 4, 4, 4, 4] - todas capturam o último valor de x
# Com generator expression
gen_funcs = ((lambda: x) for x in range(5))
print([f() for f in gen_funcs]) # [0, 1, 2, 3, 4] - avaliação lazy captura o valor atual
Cuidados com avaliação tardia em loops:
# Problema: generator expression em loop
resultados = []
for i in range(3):
# O generator expression captura a referência de i, não seu valor
resultados.append((x for x in range(i)))
# Quando iteramos, i já é 2
for gen in resultados:
print(list(gen)) # [0, 1], [0, 1], [0, 1] - todos usam i=2
# Solução: forçar a captura do valor atual
resultados = []
for i in range(3):
resultados.append((x for x in range(i))) # i é avaliado no momento da criação
# Na prática, isso ainda não resolve o problema acima
7. Generator Expressions vs Generator Functions (yield)
Ambos produzem iteradores lazy, mas diferem em complexidade e legibilidade:
# Generator expression - simples e concisa
quadrados = (x**2 for x in range(100))
# Generator function equivalente
def gerar_quadrados(n):
for x in range(n):
yield x**2
quadrados_func = gerar_quadrados(100)
# Ambos funcionam da mesma forma
print(list(quadrados)[:5]) # [0, 1, 4, 9, 16]
print(list(quadrados_func)[:5]) # [0, 1, 4, 9, 16]
Quando usar cada um:
# Generator expression: para transformações simples e diretas
soma_pares = sum(x for x in range(1000) if x % 2 == 0)
# Generator function: para lógica complexa com múltiplos yields
def fibonacci_ate_limite(limite):
a, b = 0, 1
while a < limite:
yield a
a, b = b, a + b
# Generator function com estado e lógica condicional
def processar_dados_complexos(dados):
for item in dados:
if item > 100:
yield item * 2
elif item < 0:
yield abs(item)
else:
yield item
8. Boas Práticas e Integração com Outros Recursos
Combinando com itertools para pipelines eficientes:
from itertools import islice, chain, takewhile, dropwhile
# Pipeline lazy com itertools
dados = range(1000)
# Pega apenas os primeiros 10 resultados que satisfazem a condição
resultados = islice(
(x**2 for x in dados if x % 3 == 0),
10
)
print(list(resultados)) # [0, 9, 36, 81, 144, 225, 324, 441, 576, 729]
# Combinando múltiplos generators
numeros1 = (x for x in range(5))
numeros2 = (x**2 for x in range(5, 10))
combinado = chain(numeros1, numeros2)
print(list(combinado)) # [0, 1, 2, 3, 4, 25, 36, 49, 64, 81]
Uso em compreensões aninhadas e expressões condicionais:
# Generator expression aninhada
matriz = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
elementos_pares = (
elemento
for linha in matriz
for elemento in linha
if elemento % 2 == 0
)
print(list(elementos_pares)) # [2, 4, 6, 8]
Dicas de legibilidade:
# Evite generator expressions muito longas - prefira funções geradoras
# Ruim:
dados_processados = ((x, y) for x in range(100) for y in range(100) if x % 2 == 0 if y % 3 == 0 if x + y > 50)
# Bom:
def gerar_pares_filtrados(limite_x, limite_y):
for x in range(limite_x):
if x % 2 != 0:
continue
for y in range(limite_y):
if y % 3 == 0 and x + y > 50:
yield (x, y)
dados_processados = gerar_pares_filtrados(100, 100)
Generator expressions são uma ferramenta poderosa para escrever código Python eficiente e idiomático. Quando usadas corretamente, permitem processar grandes volumes de dados com consumo mínimo de memória, mantendo a legibilidade e expressividade do código.
Referências
- PEP 289 – Generator Expressions — Documentação oficial propondo a implementação de generator expressions em Python
- Python Documentation: Generator Expressions — Documentação oficial da sintaxe e semântica de generator expressions
- Real Python: Understanding Generator Expressions — Tutorial completo sobre generators e generator expressions com exemplos práticos
- Python Wiki: Generators — Guia da comunidade Python sobre generators, incluindo performance e casos de uso
- GeeksforGeeks: Generator Expressions in Python — Artigo técnico com exemplos detalhados de generator expressions e comparações com list comprehensions