Iteradores e Geradores
Você tem usado
for x in lista:para iterar dados, mas já pensou no que pode seguirin? Por querange(1000000)não ocupa memória? Iteradores e geradores são a resposta. Eles permitem lidar com fluxos de dados "infinitos" — gerados sob demanda, não armazenados na memória.
1. O que é o Protocolo de Iterador
Em Python, qualquer objeto que pode ser usado em um laço for é iterável. Eles implementam o protocolo de iterador:
Exemplo: Básico do Protocolo de Iterador
# Todos os containers são iteráveis
print(hasattr([1, 2, 3], "__iter__")) # True
print(hasattr("abc", "__iter__")) # True
print(hasattr(100, "__iter__")) # False — números não são iteráveis
# Processo de iteração manual
numbers = [1, 2, 3]
iterator = iter(numbers) # Obtém um iterador
print(next(iterator)) # 1
print(next(iterator)) # 2
print(next(iterator)) # 3
# print(next(iterator)) # StopIteration — iteração completa
Um laço for essencialmente faz isto:
Exemplo: Simulando um Laço for Manualmente
numbers = [1, 2, 3]
it = iter(numbers)
while True:
try:
value = next(it)
print(value)
except StopIteration:
break
2. Iteradores Personalizados
Implemente __iter__ e __next__ para fazer sua classe funcionar com laços for:
Exemplo: Iterador de Contagem Regressiva
class ContagemRegressiva:
"""Iterador de contagem regressiva"""
def __init__(self, inicio):
self.current = inicio
def __iter__(self):
return self # Iterador retorna a si mesmo
def __next__(self):
if self.current <= 0:
raise StopIteration # Para a iteração
value = self.current
self.current -= 1
return value
# Uso
for i in ContagemRegressiva(5):
print(i, end=" ") # 5 4 3 2 1
Exemplo: Iterador de Fibonacci (Dificuldade ⭐⭐)
class Fibonacci:
"""Iterador de Fibonacci — gera os primeiros N números"""
def __init__(self, count):
self.count = count
self.index = 0
self.a, self.b = 0, 1
def __iter__(self):
return self
def __next__(self):
if self.index >= self.count:
raise StopIteration
self.index += 1
result = self.a
self.a, self.b = self.b, self.a + self.b
return result
for num in Fibonacci(10):
print(num, end=" ") # 0 1 1 2 3 5 8 13 21 34
3. Geradores: yield
Escrever uma classe com __iter__ e __next__ é tedioso. Funções geradoras usam yield para alcançar o mesmo resultado:
Exemplo: Gerador de Contagem Regressiva
def contagem_regressiva(inicio):
"""Gerador de contagem regressiva"""
while inicio > 0:
yield inicio
inicio -= 1
# Uso
for i in contagem_regressiva(5):
print(i, end=" ") # 5 4 3 2 1
yield é diferente de return — return encerra uma função, enquanto yield pausa ela, lembra o estado atual e retoma de onde parou na próxima chamada.
Exemplo: Gerador de Fibonacci
# Mesmo Fibonacci — usando yield, muito mais conciso que a classe iteradora
def fibonacci(count):
a, b = 0, 1
for _ in range(count):
yield a
a, b = b, a + b
for num in fibonacci(10):
print(num, end=" ") # 0 1 1 2 3 5 8 13 21 34
4. Carregamento Preguiçoso com Geradores
A maior vantagem dos geradores — eles não geram todos os dados de uma vez, mas sob demanda:
Exemplo: Carregamento Preguiçoso de Gerador
def generate_numbers():
"""Simula geração de números um por um"""
print("Gerando 1")
yield 1
print("Gerando 2")
yield 2
print("Gerando 3")
yield 3
gen = generate_numbers()
print("Gerador criado, mas ainda não executado")
print(next(gen)) # Gerando 1 \n 1
print(next(gen)) # Gerando 2 \n 2
print(next(gen)) # Gerando 3 \n 3
Vê o fluxo de execução? Cada chamada a next() executa até o próximo yield, depois pausa. Isso é incrivelmente útil para grandes dados:
Exemplo: Lendo Grandes Arquivos Linha por Linha
# Forma "preguiçosa" de processar grandes arquivos
def read_large_file(filename):
"""Lê um grande arquivo linha por linha, sem carregar tudo na memória"""
with open(filename, "r", encoding="utf-8") as f:
for line in f:
yield line.strip()
# Processa com gerador — apenas uma linha na memória por vez
for line in read_large_file("huge_log.txt"):
if "ERRO" in line:
print(line) # Velocidade limitada apenas pela velocidade de leitura do disco
5. Expressões Geradoras
Similar a list comprehensions, mas com parênteses — expressões geradoras são avaliadas preguiçosamente:
Exemplo: Expressão Geradora
# List comprehension — gera todos os dados de uma vez
squares_list = [x ** 2 for x in range(1000000)]
print(f"Tamanho da lista: {len(squares_list)}") # 1000000 (usa memória)
# Expressão geradora — sob demanda
squares_gen = (x ** 2 for x in range(1000000))
print(f"Gerador: {squares_gen}") # generator object (usa quase nenhuma memória)
# Mesmo uso — laço for
for i, val in enumerate(squares_gen):
if i >= 5:
break
print(val, end=" ") # 0 1 4 9 16
| Comparação | List Comprehension [] |
Expressão Geradora () |
|---|---|---|
| Memória | Todos os dados na memória | Sob demanda, quase nenhuma |
| Velocidade | Geração e acesso rápidos | Preguiçosa, computa cada vez |
| Reutilizável | Pode iterar várias vezes | Só pode iterar uma vez |
| Caso de uso | Dados pequenos, uso repetido | Grandes dados, passagem única |
Casos de Uso Comuns
- Processamento de grandes dados: Leia grandes arquivos de log/CSV linha por linha, evitando estouro de memória.
- Sequências infinitas: Geradores podem representar fluxos de dados "infinitos" — Fibonacci, dados de sensores.
- Processamento em pipeline: Encadeie múltiplos geradores em um pipeline de processamento de dados.
- Iteração reversa:
reversed()retorna um iterador para alguns tipos. - Avaliação preguiçosa: Funções retornam um gerador; o chamador decide quantos valores consumir.
❓ Perguntas Frequentes
P: Qual a diferença entre um gerador e uma função regular? R: Uma função regular usa
returnpara retornar um resultado uma vez e termina. Um gerador usayieldpara produzir valores um de cada vez, pausando e salvando o estado após cada yield. Cada chamada a uma função geradora retorna um novo objeto gerador. P: Geradores só podem ser iterados uma vez. E se eu precisar iterar várias vezes? R: Uselist(gen)para converter o gerador em uma lista. Mas se os dados forem muito grandes, converter para lista esgotará a memória. Um compromisso: useitertools.tee()para copiar o gerador, ou chame a função geradora novamente para criar um novo. P:yieldereturnpodem coexistir? R: Sim —returnem um gerador efetivamente para a iteração e levantaStopIterationcom um valor opcional. Mas é raramente usado. Se você encontrar uma função que precisa tanto deyieldquanto dereturn, repense o design.
📖 Resumo
- Objetos iteráveis suportam laços
for; nos bastidores, implementam o protocolo__iter__e__next__ - Iteração manual:
iter()obtém um iterador,next()obtém o próximo valor,StopIterationencerra - Funções geradoras usam
yieldpara produzir valores um de cada vez, salvando estado automaticamente - Geradores são preguiçosos — produzem o necessário, sem desperdício de memória
- Expressões geradoras
(x for x in seq)são como list comprehensions, mas avaliadas preguiçosamente yield fromdelega a outro gerador (avançado, aprenda quando precisar)
📝 Atividades
-
Básico (Dificuldade ⭐): Escreva um gerador
even_numbers(max_n)que gera todos os números pares de 0 atémax_n. -
Intermediário (Dificuldade ⭐⭐): Escreva um gerador
repeat_list(items, times)que repete cada elemento de uma listatimesvezes. Por exemplo,list(repeat_list([1, 2, 3], 2))retorna[1, 1, 2, 2, 3, 3]. -
Desafio (Dificuldade ⭐⭐⭐): Escreva um gerador
deep_flatten(nested)que achata listas arbitrariamente aninhadas. Por exemplo,list(deep_flatten([1, [2, [3, 4]], 5]))retorna[1, 2, 3, 4, 5]. Dica: Itere sobre os elementos; se um elemento for uma lista, gere recursivamente a partir dele comyield from.



