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