Generators e a palavra-chave yield

1. Introdução aos Generators

Generators são funções especiais em Python que produzem sequências de valores sob demanda, em vez de computar todos os valores de uma vez e armazená-los em memória. A diferença fundamental entre uma função comum e um generator é que o generator preserva seu estado entre chamadas sucessivas.

# Função comum: retorna uma lista completa
def numeros_ao_quadrado(n):
    return [x**2 for x in range(n)]

# Generator: produz valores sob demanda
def generator_quadrados(n):
    for x in range(n):
        yield x**2

O benefício imediato é a economia de memória. Enquanto uma função comum aloca espaço para todos os resultados simultaneamente, um generator produz cada valor apenas quando solicitado, tornando-o ideal para trabalhar com grandes volumes de dados.

2. A Palavra-chave yield

A palavra-chave yield é o coração dos generators. Diferentemente de return, que encerra a função definitivamente, yield pausa a execução, retorna um valor e salva o estado atual da função para ser retomada posteriormente.

def contador_regressivo(n):
    while n > 0:
        yield n
        n -= 1
    yield "Fim!"

gen = contador_regressivo(3)
print(next(gen))  # 3
print(next(gen))  # 2
print(next(gen))  # 1
print(next(gen))  # Fim!

É possível ter múltiplos yield em uma mesma função, criando uma sequência de valores que será produzida à medida que o generator for consumido.

3. Criando e Consumindo Generators

Criar um generator é simples: escreva uma função contendo pelo menos um yield. Para consumi-lo, use next() ou itere com for.

def fibonacci(limite):
    a, b = 0, 1
    while a < limite:
        yield a
        a, b = b, a + b

# Consumindo com next()
fib = fibonacci(10)
print(next(fib))  # 0
print(next(fib))  # 1
print(next(fib))  # 1

# Consumindo com for (mais seguro - trata StopIteration automaticamente)
for valor in fibonacci(10):
    print(valor, end=" ")  # 0 1 1 2 3 5 8

O ciclo de vida de um generator inclui criação, pausa (após cada yield), retomada (quando next() é chamado) e exaustão, que levanta StopIteration.

4. Generator Expressions

Generator expressions oferecem uma sintaxe concisa para criar generators, similar às list comprehensions, mas usando parênteses.

# List comprehension (eager evaluation - avalia tudo imediatamente)
lista_quadrados = [x**2 for x in range(1000)]

# Generator expression (lazy evaluation - avalia sob demanda)
gen_quadrados = (x**2 for x in range(1000))

print(type(gen_quadrados))  # <class 'generator'>
print(next(gen_quadrados))  # 0
print(next(gen_quadrados))  # 1

Use generator expressions quando os dados forem grandes demais para caber em memória ou quando você precisar processar apenas parte da sequência. Use list comprehensions quando precisar acessar elementos aleatoriamente ou múltiplas vezes.

5. Comunicação Bidirecional com Generators

Generators suportam comunicação bidirecional através do método send(), que envia valores para dentro do generator, e yield usado como expressão para recebê-los.

def corrotina_simples():
    print("Corrotina iniciada")
    while True:
        valor = yield
        print(f"Recebido: {valor}")

gen = corrotina_simples()
next(gen)  # Inicializa o generator
gen.send("Olá")  # Recebido: Olá
gen.send("Mundo")  # Recebido: Mundo

Exemplo prático de uma calculadora simples:

def acumuladora():
    total = 0
    while True:
        valor = yield total
        if valor is not None:
            total += valor

calc = acumuladora()
next(calc)  # Inicializa
print(calc.send(10))  # 10
print(calc.send(5))   # 15
print(calc.send(3))   # 18

6. Métodos Avançados: throw e close

O método throw() permite injetar exceções no generator, enquanto close() o finaliza prematuramente.

def manipulador_excecoes():
    try:
        while True:
            valor = yield
            print(f"Processando: {valor}")
    except ValueError:
        print("Valor inválido detectado!")
    finally:
        print("Generator finalizado")

gen = manipulador_excecoes()
next(gen)
gen.send(42)  # Processando: 42
gen.throw(ValueError)  # Valor inválido detectado! Generator finalizado

# Usando close()
gen2 = manipulador_excecoes()
next(gen2)
gen2.close()  # Generator finalizado

7. Generators como Pipelines de Processamento

Uma das aplicações mais poderosas de generators é criar pipelines de processamento de dados, onde a saída de um generator alimenta a entrada do próximo.

# Pipeline de processamento de números
def ler_numeros():
    for i in range(1, 101):
        yield i

def filtrar_pares(entrada):
    for num in entrada:
        if num % 2 == 0:
            yield num

def transformar(entrada):
    for num in entrada:
        yield num ** 2

def limitar(entrada, maximo):
    contador = 0
    for num in entrada:
        if contador >= maximo:
            break
        yield num
        contador += 1

# Pipeline completo
pipeline = limitar(
    transformar(
        filtrar_pares(
            ler_numeros()
        )
    ),
    5
)

for resultado in pipeline:
    print(resultado, end=" ")  # 4 16 36 64 100

Cada etapa do pipeline processa dados sob demanda, sem criar listas intermediárias, resultando em código eficiente e escalável.

8. Casos de Uso Reais e Boas Práticas

Leitura de arquivos grandes:

def ler_linhas_arquivo(nome_arquivo):
    with open(nome_arquivo, 'r') as arquivo:
        for linha in arquivo:
            yield linha.strip()

# Processa milhões de linhas sem sobrecarregar a memória
for linha in ler_linhas_arquivo('grande_arquivo.txt'):
    if 'erro' in linha.lower():
        print(f"Erro encontrado: {linha}")

Sequências infinitas:

def gerador_ids():
    id_atual = 0
    while True:
        yield id_atual
        id_atual += 1

ids = gerador_ids()
print(next(ids))  # 0
print(next(ids))  # 1
print(next(ids))  # 2

Boas práticas:
- Use generators quando trabalhar com grandes conjuntos de dados
- Prefira generator expressions para transformações simples
- Evite usar send() a menos que precise de comunicação bidirecional
- Lembre-se que generators são consumíveis: uma vez exauridos, não podem ser reutilizados
- Combine generators em pipelines para processamento eficiente

Generators transformam a maneira como pensamos sobre iteração em Python, oferecendo uma ferramenta elegante e eficiente para processamento lazy de dados.

Referências