Iteradores e Geradores

Você tem usado for x in lista: para iterar dados, mas já pensou no que pode seguir in? Por que range(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

PYTHON
# 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
▶ Experimente

Um laço for essencialmente faz isto:

Exemplo: Simulando um Laço for Manualmente

PYTHON
numbers = [1, 2, 3]
it = iter(numbers)
while True:
    try:
        value = next(it)
        print(value)
    except StopIteration:
        break
▶ Experimente

2. Iteradores Personalizados

Implemente __iter__ e __next__ para fazer sua classe funcionar com laços for:

Exemplo: Iterador de Contagem Regressiva

PYTHON
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
▶ Experimente

Exemplo: Iterador de Fibonacci (Dificuldade ⭐⭐)

PYTHON
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
▶ Experimente

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

PYTHON
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
▶ Experimente

yield é diferente de returnreturn 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

PYTHON
# 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
▶ Experimente
💡 Geradores vs Classes Iteradoras: Funções geradoras (yield) são geralmente muito mais concisas que escrever classes iteradoras manualmente. Na maioria dos cenários, geradores são a primeira escolha. Considere escrever uma classe iteradora apenas se precisar manter estado complexo ou precisar de métodos adicionais.


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

PYTHON
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
▶ Experimente

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

PYTHON
# 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
▶ Experimente

5. Expressões Geradoras

Similar a list comprehensions, mas com parênteses — expressões geradoras são avaliadas preguiçosamente:

Exemplo: Expressão Geradora

PYTHON
# 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
▶ Experimente
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
💡 Quando usar geradores? ① Grandes conjuntos de dados (milhares+ itens). ② Só precisa iterar uma vez. ③ Precisa de processamento em fluxo (arquivo linha por linha, fluxos de dados em tempo real). ④ Quer expressar "sequências infinitas."


Casos de Uso Comuns


❓ Perguntas Frequentes

P: Qual a diferença entre um gerador e uma função regular? R: Uma função regular usa return para retornar um resultado uma vez e termina. Um gerador usa yield para 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: Use list(gen) para converter o gerador em uma lista. Mas se os dados forem muito grandes, converter para lista esgotará a memória. Um compromisso: use itertools.tee() para copiar o gerador, ou chame a função geradora novamente para criar um novo. P: yield e return podem coexistir? R: Sim — return em um gerador efetivamente para a iteração e levanta StopIteration com um valor opcional. Mas é raramente usado. Se você encontrar uma função que precisa tanto de yield quanto de return, repense o design.


📖 Resumo


📝 Atividades

  1. Básico (Dificuldade ⭐): Escreva um gerador even_numbers(max_n) que gera todos os números pares de 0 até max_n.

  2. Intermediário (Dificuldade ⭐⭐): Escreva um gerador repeat_list(items, times) que repete cada elemento de uma lista times vezes. Por exemplo, list(repeat_list([1, 2, 3], 2)) retorna [1, 1, 2, 2, 3, 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 com yield from.

100%