Quando falamos de Python Generators, estamos falando de uma das funcionalidades mais poderosas e elegantes da linguagem. Geradores permitem criar sequências de dados sob demanda, sem necessidade de armazenar tudo na memória de uma vez. Isso é especialmente útil quando trabalhamos com grandes volumes de dados ou streams infinitos.

Neste guia completo, você vai aprender desde o básico até técnicas avançadas de generators em Python, com exemplos práticos que você pode aplicar imediatamente em seus projetos.

O Que São Python Generators?

Generators são funções especiais que usam a palavra-chave yield para retornar valores de forma lazy (preguiçosa). Diferente de funções normais que retornam todos os valores de uma vez com return, generators pausam a execução e retornam um valor por vez, mantendo o estado entre as chamadas.

A principal vantagem dos generators é a memória eficiente. Enquanto uma lista normal carrega todos os elementos na memória, um generator produz cada elemento apenas quando solicitado. Isso é fundamental para processar arquivos grandes, streams de dados ou sequências infinitas.

Diferença Entre Funções Normais e Generators

Vamos entender na prática a diferença:

# Função normal - retorna tudo de uma vez
def funcao_normal(n):
    resultado = []
    for i in range(n):
        resultado.append(i * 2)
    return resultado

# Generator - retorna sob demanda
def generator_simples(n):
    for i in range(n):
        yield i * 2

# Usando função normal
print(funcao_normal(5))  # [0, 2, 4, 6, 8] - tudo na memória

# Usando generator
gen = generator_simples(5)
print(next(gen))  # 0
print(next(gen))  # 2
print(next(gen))  # 4

Note que o generator não executa nenhuma linha quando é chamado - ele só retorna um objeto iterador. Cada chamada de next() executa o código até o próximo yield.

Fonte: Python Documentation - Generators

Criando Seu Primeiro Generator

A sintaxe de um generator é muito similar a uma função normal, mas com uma diferença crucial: usar yield em vez de return.

def contador(maximo):
    """Generator que conta de 0 até maximo-1"""
    contador = 0
    while contador < maximo:
        yield contador
        contador += 1

# Usando o generator
for numero in contador(5):
    print(f"Contagem: {numero}")

Este código produz:

Contagem: 0
Contagem: 1
Contagem: 2
Contagem: 3
Contagem: 4

Agora você deve estar se perguntando: "Isso parece muito com uma lista comprehension". E você está certo! Generators são conceito relacionado, mas com uma diferença importante.

Fonte: Real Python - Introduction to Python Generators

Generator Expressions

Assim como list comprehensions, generators têm sua versão mais concisa: as generator expressions. A sintaxe é quase idêntica, mas usam parênteses em vez de colchetes.

# List comprehension - cria lista completa na memória
lista = [x**2 for x in range(1000000)]  # Problema com memória!

# Generator expression - cria iterador preguiçoso
gen = (x**2 for x in range(1000000))  # Eficiente!

# Usando o generator
print(sum(x**2 for x in range(10)))  # 285

Generator expressions são perfeitas quando você precisa de uma sequência simples e não precisa armazenar todos os valores. Use-as em funções que esperam iteráveis, como sum(), max(), min(), etc.

Fonte: W3Schools - Python Generators

Por Que Usar Generators?

Agora que você já sabe como criar generators, vamos entender por que eles são tão importantes:

1. Eficiência de Memória

O benefício mais óbvio dos generators é a economia de memória. Vamos comparar:

import sys

# Lista de 1000 números
lista = [i for i in range(1000)]
print(f"Tamanho da lista: {sys.getsizeof(lista)} bytes")

# Generator equivalente
gen = (i for i in range(1000))
print(f"Tamanho do generator: {sys.getsizeof(gen)} bytes")

O generator ocupa muito menos memória porque não precisa armazenar todos os valores - ele os gera sob demanda.

2. Performance Superior

Para operações que não precisam de todos os dados de uma vez, generators são mais rápidos porque não hanno custo inicial de criação de toda a estrutura de dados.

import time

# Medindo tempo - lista vs generator
inicio = time.time()
soma_lista = sum([i for i in range(10000000)])
tempo_lista = time.time() - inicio

inicio = time.time()
soma_gen = sum(i for i in range(10000000))
tempo_gen = time.time() - inicio

print(f"Lista: {tempo_lista:.4f}s")
print(f"Generator: {tempo_gen:.4f}s")

3. Representação de Streams Infinitos

Generators podem representar sequências infinitas, algo impossível com listas normais:

def numeros_fibonacci():
    """Generator infinito de números de Fibonacci"""
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b

fib = numeros_fibonacci()
for i in range(10):
    print(next(fib), end=" ")  # 0 1 1 2 3 5 8 13 21 34

Este generator pode rodar para sempre porque não armazena valores - ele calcula cada um sob demanda.

Fonte: Stack Overflow - Python Generator Use Cases

Generators Avançados

Delegação com yield from

O yield from permite delegar a execução para outro generator, criando código mais modular:

def gerar_numeros():
    """Generator principal que delega para outros"""
    yield from range(5)           # 0, 1, 2, 3, 4
    yield from ['a', 'b', 'c']    # a, b, c
    yield from (x*2 for x in range(3))  # 0, 2, 4

for item in gerar_numeros():
    print(item, end=" ")  # 0 1 2 3 4 a b c 0 2 4

Esta técnica é extremamente útil quando você precisa combinar múltiplos generators ou iteradores.

Enviando Valores para Generators

Generators podem receber valores através do método send():

def gerenciador_estado():
    """Generator que收到了 valores e continua"""
    valor = yield "Iniciando..."
    yield f"Recebido: {valor}"
    yield "Finalizado"

gen = gerenciador_estado()
print(next(gen))        # Iniciando...
print(gen.send("Olá"))  # Recebido:Olá
print(next(gen))        # Finalizado

Esta funcionalidade permite criar generators com estado interno complexo, útil para implementar máquinas de estado ou corrotinas.

Fonte: GeeksforGeeks - Generators in Python

Pipeline de Generators

Uma das aplicações mais poderosas de generators é criar pipelines de processamento de dados, similar ao Unix pipes:

def ler_numeros():
    """Simula leitura de números"""
    for i in range(1, 101):
        yield i

def filtrar_pares(numeros):
    """Filtra apenas números pares"""
    for num in numeros:
        if num % 2 == 0:
            yield num

def dobrar_valores(numeros):
    """Dobra cada valor"""
    for num in numeros:
        yield num * 2

def limitar(numeros, n):
    """Limita a quantidade de resultados"""
    for i, num in enumerate(numeros):
        if i >= n:
            break
        yield num

# Criando o pipeline
pipeline = limitar(dobrar_valores(filtrar_pares(ler_numeros())), 10)

print("Pipeline result:", list(pipeline))
# [4, 8, 12, 16, 20, 24, 28, 32, 36, 40]

Este padrão é extremamente eficiente para processar grandes volumes de dados, porque cada etapa só processa dados conforme a próxima etapa os solicita.

Generators vs Iteradores

É importante não confundir generators com iteradores. Enquanto generators são uma forma de criar iteradores, nem todo iterador é um generator:

# Generator - usa a palavra-chave yield
def gen():
    yield 1
    yield 2
    yield 3

# Iterador manual - usa __iter__ e __next__
class Iterador:
    def __init__(self):
        self.valor = 0

    def __iter__(self):
        return self

    def __next__(self):
        self.valor += 1
        if self.valor > 3:
            raise StopIteration
        return self.valor

A vantagem dos generators é que são muito mais simples de escrever - você não precisa criar uma classe completa!

Fonte: Programiz - Python Generator

Casos de Uso Reais

1. Processamento de Arquivos Grandes

def ler_arquivo_grande(caminho):
    """Lê arquivo linha por linha sem carregar tudo na memória"""
    with open(caminho, 'r') as arquivo:
        for linha in arquivo:
            yield linha.strip()

# Uso eficiente com arquivos de GBs
for linha in ler_arquivo_grande('arquivo_gigante.txt'):
    processar(linha)

2. Web Scraping Pagination

def buscar_paginas(url_base, paginas):
    """Simula busca de múltiplas páginas"""
    for pagina in range(1, paginas + 1):
        dados = fetch(f"{url_base}?page={pagina}")
        yield dados

3. Processamento de Dados em Tempo Real

def processar_stream_sensor():
    """Simula leitura de sensores em tempo real"""
    import random
    while True:
        yield random.randint(0, 100)

# Processa dados conforme chegam
sensor = processar_stream_sensor()
for leitura in sensor:
    if leitura > 80:
        print(f"Alerta: {leitura}")

Métricas e raise em Generators

Generators também suportam tratamento de exceções:

def generator_com_tratamento():
    """Generator com try-except"""
    try:
        for i in range(10):
            yield i
    except ValueError:
        yield "Erro capturado!"

# Você pode lançar exceções em generators
gen = generator_com_tratamento()
for _ in range(5):
    print(next(gen))

gen.throw(ValueError)  # Lança exceção no generator

Boas Práticas com Generators

Agora aqui estão algumas práticas recomendadas ao trabalhar com generators:

  • Use generators para dados grandes - Quando você tem milhões de registros, generators são essenciais.
  • Documente seu generator - Escreva o que ele produz e quais são seus parâmetros.
  • Evite efectos colaterais dentro de generators - Eles funcionam melhor quando são puros.
  • Combine com itertools - A biblioteca padrão oferece muitas ferramentas complementares.
  • Use generator expressions quando possível - São mais concisas para casos simples.
from itertools import islice, count

# Exemplo avançado com itertools
def numeros_primos():
    """Generator de números primos"""
    primos = []
    for n in count(2):
        if all(n % p != 0 for p in primos[:int(n**0.5)+1]):
            primos.append(n)
            yield n

# Pegar os 10 primeiros primos
print(list(islice(numeros_primos(), 10)))  # [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]

Fonte: Python itertools Documentation

Conclusão

Python Generators são uma ferramenta fundamental para qualquer desenvolvedor Python que trabalha com dados. Eles oferecem uma forma elegante e eficiente de criar iteradores, processar grandes volumes de dados e implementar padrões de programação funcional.

Dominar generators abre portas para escrever código mais limpo, eficiente e escalável. Seja processando arquivos de GBs, implementando pipelines de dados ou criando streams infinitos, generators são a solução certa.

Continue aprendendo com os guias gratuitos do Universo Python: explore decorators e generators avançados, programação assíncrona, e muito mais para elevar seu código ao próximo nível!