Se você já escreveu um loop for em Python, you já usou iteradores — mesmo sem saber. Mas quando você precisa processar grandes volumes de dados, criar fluxos infinitos ou simplesmente escrever código mais elegante, entender geradores e iteradores é essencial.
Neste guia definitivo, você vai aprender o que são iteradores e geradores, como funcionam internamente, e como usá-los para transformar a forma como você escreve Python. Com exemplos práticos, você verá como essas ferramentas podem reduzir o consumo de memória, simplificar seu código e abrir portas para padrões de programação mais avançados.
O Que São Iteradores?
Em Python, um iterador é um objeto que implementa o protocolo de iteração: os métodos __iter__() e __next__(). O primeiro retorna o próprio iterador, e o segundo retorna o próximo elemento da sequência. Quando não há mais elementos, __next__() lança a exceção StopIteration.
Praticamente todas as estruturas de dados em Python são iteráveis: listas, tuplas, dicionários, conjuntos, strings. Quando você escreve for item in lista, o Python chama internamente iter(lista) para obter um iterador e depois chama next() em cada iteração.
lista = [10, 20, 30]
iterador = iter(lista)
print(next(iterador)) # 10
print(next(iterador)) # 20
print(next(iterador)) # 30
# print(next(iterador)) # StopIteration
Entender esse mecanismo é o primeiro passo para criar seus próprios iteradores personalizados e, mais importante, para dominar os geradores, que são a forma mais elegante de criar iteradores em Python.
O Protocolo de Iteração
Qualquer objeto que tenha os métodos __iter__ e __next__ é considerado um iterador. O __iter__ deve retornar o próprio objeto iterador, e o __next__ deve retornar o próximo valor ou lançar StopIteration.
class Contador:
def __init__(self, limite):
self.limite = limite
self.atual = 0
def __iter__(self):
return self
def __next__(self):
if self.atual >= self.limite:
raise StopIteration
valor = self.atual
self.atual += 1
return valor
for numero in Contador(5):
print(numero) # 0, 1, 2, 3, 4
Embora funcione perfeitamente, criar iteradores com classes é verboso. É aí que os geradores entram em cena.
O Que São Geradores?
Um gerador é uma função especial que usa a palavra-chave yield em vez de return. Diferente de uma função normal que executa e termina, uma função geradora mantém seu estado entre chamadas — ela "pausa" no yield e retoma de onde parou na próxima chamada.
Quando você chama uma função geradora, ela não executa imediatamente. Em vez disso, retorna um objeto gerador (que é um iterador). Cada vez que você chama next() nele, a função executa até o próximo yield e retorna o valor.
def contador_simples():
print("Começando...")
yield 1
print("Continuando...")
yield 2
print("Finalizando...")
yield 3
gen = contador_simples()
print(next(gen)) # Começando... 1
print(next(gen)) # Continuando... 2
print(next(gen)) # Finalizando... 3
Essa é a magia dos geradores: eles permitem computação preguiçosa (lazy evaluation), ou seja, os valores são gerados sob demanda, apenas quando necessários.
Yield vs Return: Qual a Diferença?
A diferença fundamental entre yield e return é o comportamento de estado:
return: encerra a função permanentemente e retorna um valor.yield: pausa a função, salva todo o seu estado (variáveis locais, posição no código) e retorna um valor. Na próxima chamada, a função retoma exatamente de onde parou.
Uma função geradora pode ter múltiplos yield e pode até misturar yield com return. Quando return é usado em um gerador, ele encerra a iteração levantando StopIteration, e o valor passado ao return fica disponível na exceção (embora isso seja raramente usado).
Para um mergulho técnico completo, consulte a PEP 255 — Simple Generators, que introduziu oficialmente os geradores no Python 2.2.
Generator Expressions
Assim como você tem list comprehensions para criar listas de forma concisa, existem as generator expressions para criar geradores. A sintaxe é quase idêntica, mas usa parênteses em vez de colchetes.
# List comprehension — cria uma lista completa na memória
quadrados_lista = [x**2 for x in range(1000000)] # ~ 8 MB de memória
# Generator expression — cria um gerador preguiçoso
quadrados_gen = (x**2 for x in range(1000000)) # ~ 56 bytes de memória
print(next(quadrados_gen)) # 0
print(next(quadrados_gen)) # 1
print(next(quadrados_gen)) # 4
A diferença de memória é astronômica: a list comprehension aloca espaço para um milhão de inteiros de uma vez, enquanto a generator expression cria um gerador que ocupa alguns bytes e calcula cada valor sob demanda.
As generator expressions foram introduzidas no Python 2.4 pela PEP 289 — Generator Expressions, que detalha a motivação e o design dessa feature.
Se você quer entender melhor as list comprehensions antes de migrar para generator expressions, confira nosso guia completo: List Comprehension em Python: Guia Completo.
O Operador Yield From
Introduzido no Python 3.3 (PEP 380), o yield from permite que um gerador delegue iterações para outro gerador ou iterável. Isso simplifica a composição de geradores e evita aninhamentos verbosos.
def gerador_principal():
yield from [1, 2, 3]
yield from range(4, 7)
yield from "ab"
for valor in gerador_principal():
print(valor, end=" ") # 1 2 3 4 5 6 a b
Sem yield from, you teria que iterar manualmente sobre cada sub-iterador:
def gerador_sem_yield_from():
for item in [1, 2, 3]:
yield item
for item in range(4, 7):
yield item
for item in "ab":
yield item
O yield from também lida automaticamente com a comunicação bidirecional entre geradores — valores enviados com send() são propagados para o gerador delegado, e exceções são transmitidas corretamente.
Geradores Infinitos
Uma das aplicações mais impressionantes de geradores é a criação de sequências infinitas. Como os valores são computados sob demanda, você pode representar sequências teoricamente infinitas sem consumir memória infinita.
def fibonacci_infinito():
a, b = 0, 1
while True:
yield a
a, b = b, a + b
fib = fibonacci_infinito()
for _ in range(10):
print(next(fib), end=" ") # 0 1 1 2 3 5 8 13 21 34
Com geradores infinitos, você controla exatamente quantos valores consumir. O gerador não sabe que é "infinito" — ele simplesmente continua produzindo valores enquanto você continuar chamando next().
O módulo itertools da biblioteca padrão oferece diversas ferramentas para trabalhar com geradores e iteradores, incluindo funções como islice() que permite "fatiar" geradores infinitos.
Aplicações Práticas
1. Processamento de Grandes Arquivos
Geradores brilham quando você precisa processar arquivos maiores que a memória disponível. Um gerador pode ler e processar o arquivo linha por linha:
def linhas_arquivo(caminho):
with open(caminho, 'r', encoding='utf-8') as arquivo:
for linha in arquivo:
yield linha.strip()
def linhas_com_palavra(linhas, palavra):
for linha in linhas:
if palavra in linha:
yield linha
def contar_ocorrencias(linhas, palavra):
return sum(1 for linha in linhas if palavra in linha)
# Uso encadeado — memória constante!
total = contar_ocorrencias(linhas_arquivo("dados.csv"), "Python")
print(f"Ocorrências: {total}")
Cada gerador faz apenas uma coisa e faz bem. O encadeamento mantém o uso de memória constante — em nenhum momento todo o arquivo está na RAM.
2. Pipelines de Dados
Geradores são ideais para construir pipelines de processamento onde cada etapa é um gerador independente:
def ler_sensores():
import random
while True:
yield random.uniform(20, 30)
def filtrar_valores(dados, minimo, maximo):
for valor in dados:
if minimo <= valor <= maximo:
yield valor
def normalizar(dados, minimo, maximo):
for valor in dados:
yield (valor - minimo) / (maximo - minimo)
pipeline = normalizar(
filtrar_valores(ler_sensores(), 22, 28),
22, 28
)
for _ in range(5):
print(f"{next(pipeline):.3f}")
3. Lazy Evaluation em Data Science
Em ciência de dados, geradores evitam carregar datasets inteiros em memória. Bibliotecas como pandas integram-se bem com geradores para processamento de dados em streaming.
def batches(dados, tamanho):
"""Divide um iterável em lotes de tamanho fixo."""
batch = []
for item in dados:
batch.append(item)
if len(batch) == tamanho:
yield batch
batch = []
if batch:
yield batch
dados = range(100)
for i, lote in enumerate(batches(dados, 10)):
print(f"Lote {i}: {list(lote)}")
if i >= 3: # Processar só os primeiros 4 lotes
break
Iteração Bidirecional com send()
Geradores não são apenas produtores unidirecionais de dados. O método send() permite enviar valores de volta para o gerador, que os recebe como o valor da expressão yield. Isso transforma geradores em corrotinas simples.
def corretor():
print("Aguardando correção...")
while True:
valor = yield
print(f"Corrigindo: {valor ** 2}")
c = corretor()
next(c) # Inicializa o gerador (chega até o primeiro yield)
c.send(5) # Corrigindo: 25
c.send(10) # Corrigindo: 100
Embora o async/await tenha substituído esse padrão para corrotinas modernas, entender send() é útil para depuração e para padrões específicos de comunicação entre geradores.
Comparação: Geradores vs Listas vs Iteradores
| Característica | Lista | Iterador (classe) | Gerador |
|---|---|---|---|
| Memória | Alta (tudo na RAM) | Baixa | Baixíssima |
| Reiterável | Sim | Não (usa-se uma vez) | Não (usa-se uma vez) |
| Acesso aleatório | Sim (índices) | Não | Não |
| Sintaxe | [...] |
Verborrágica | Concisa |
| Lazy | Não | Sim | Sim |
| Pode ser infinito | Não | Sim | Sim |
Cada abordagem tem seu lugar. Listas são ótimas para coleções pequenas ou quando você precisa de acesso aleatório. Iteradores são apropriados quando você precisa de controle fino. Geradores são a escolha ideal para a maioria dos casos de processamento sequencial, combinando simplicidade com eficiência.
Geradores na Prática: Exemplo de Web Scraping
Vamos ver um exemplo prático que combina vários conceitos. Suponha que você precisa raspar múltiplas páginas de um site e extrair informações:
import time
def paginas_api(base_url, total_paginas):
"""Gera URLs das páginas sob demanda."""
for i in range(1, total_paginas + 1):
yield f"{base_url}?pagina={i}"
def baixar_paginas(urls):
"""Simula download (substitua por aiohttp ou httpx)."""
for url in urls:
time.sleep(0.5) # Simula latência
yield f"Dados de {url}"
def extrair_dados(respostas):
"""Extrai informações relevantes de cada resposta."""
for resposta in respostas:
yield {
"fonte": resposta,
"itens": len(resposta),
"timestamp": time.time()
}
urls = paginas_api("https://api.exemplo.com/dados", 100)
respostas = baixar_paginas(urls)
dados = extrair_dados(respostas)
# Consome apenas 20 páginas, sem desperdício
for i, item in enumerate(dados):
if i >= 20:
break
print(f"{item['fonte']} - {item['itens']} itens")
Esse padrão de pipeline com geradores mantém o código limpo, modular e eficiente. Cada etapa é testável independentemente e o consumo de memória é mínimo.
Se você trabalha com web scraping, veja também nosso artigo sobre Lambda, Map, Filter e Reduce em Python — funções que se integram perfeitamente com geradores.
Dicas e Boas Práticas
1. Prefira Geradores a Listas Grandes
Sempre que você não precisar acessar os dados mais de uma vez ou precisar de acesso aleatório, prefira geradores a listas. A diferença de performance e memória é significativa, especialmente com grandes volumes.
2. Use itertools para Composição
O módulo itertools oferece funções como chain(), zip_longest(), islice(), takewhile() e dropwhile() que compõem perfeitamente com geradores. Combinar itertools com generator expressions é uma das formas mais expressivas de escrever Python.
3. Cuidado com Efeitos Colaterais
Geradores são ideais para computação pura — não para operações com efeitos colaterais como escrever em arquivos ou modificar variáveis globais. Se você precisa de efeitos colaterais, seja explícito e documente.
4. Não Exagere
Para coleções pequenas (algumas dezenas de elementos), uma lista simples é perfeitamente aceitável. A otimização prematura é a raiz de todo mal — use geradores quando fizer sentido, não por dogma.
Referências e Aprofundamento
Para continuar seus estudos sobre geradores e iteradores, consulte estas fontes confiáveis:
- Documentação Oficial — Iteradores
- Documentação Oficial — Geradores
- PEP 255 — Simple Generators
- PEP 289 — Generator Expressions
- Real Python: Introduction to Python Generators
- Python Wiki: Generators
- itertools — Biblioteca Padrão
- Yield Expression — Referência da Linguagem
Conclusão
Geradores e iteradores são ferramentas fundamentais no arsenal de qualquer desenvolvedor Python. Eles permitem escrever código mais limpo, mais eficiente e mais expressivo, especialmente quando lidamos com grandes volumes de dados ou fluxos contínuos.
O segredo está no lazy evaluation: computar apenas o que é necessário, quando é necessário. Isso não só economiza memória como também torna o código mais modular e testável.
Agora que você domina os conceitos, pratique! Crie seus próprios geradores, experimente com yield from, explore o módulo itertools e veja como essas ferramentas podem transformar a qualidade do seu código Python.
Para mais conteúdo sobre Python, continue explorando o Universo Python!