Iteradores: __iter__ e __next__

1. Introdução aos Iteradores em Python

Para compreender iteradores em Python, precisamos primeiro distinguir dois conceitos fundamentais: iterável e iterador. Um objeto iterável é aquele que pode ser percorrido em um loop for, como listas, tuplas, strings e dicionários. Já um iterador é um objeto que mantém o estado da iteração e sabe como produzir o próximo valor.

Quando você escreve for item in minha_lista, o Python faz algo interessante por baixo dos panos: ele chama a função iter() no objeto iterável, que retorna um iterador. Em seguida, chama repetidamente next() nesse iterador até que a exceção StopIteration seja levantada.

A diferença crucial é que objetos iteráveis implementam __iter__, enquanto iteradores implementam tanto __iter__ quanto __next__. Todo iterador é iterável, mas nem todo iterável é um iterador.

2. O Protocolo do Iterador: __iter__ e __next__

O protocolo do iterador em Python é elegantemente simples. Vamos examinar cada método:

__iter__(self): Deve retornar o objeto iterador. Em muitos casos, retorna self se a própria classe for o iterador.

__next__(self): Deve retornar o próximo valor disponível. Quando não houver mais valores, deve levantar StopIteration.

Vejamos um exemplo mínimo de um contador manual:

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

# Uso
contador = Contador(5)
for numero in contador:
    print(numero)  # 0, 1, 2, 3, 4

3. Criando uma Classe Iterável Personalizada

Agora, vamos criar uma classe iterável que não é um iterador, mas retorna um objeto iterador separado. Isso é útil quando queremos permitir múltiplas iterações independentes.

class ListaPersonalizada:
    def __init__(self, dados):
        self.dados = dados

    def __iter__(self):
        return IteradorLista(self.dados)

class IteradorLista:
    def __init__(self, dados):
        self.dados = dados
        self.indice = 0

    def __iter__(self):
        return self

    def __next__(self):
        if self.indice >= len(self.dados):
            raise StopIteration
        valor = self.dados[self.indice]
        self.indice += 1
        return valor

# Exemplo prático: MeuRange
class MeuRange:
    def __init__(self, inicio, fim, passo=1):
        self.inicio = inicio
        self.fim = fim
        self.passo = passo

    def __iter__(self):
        return IteradorRange(self.inicio, self.fim, self.passo)

class IteradorRange:
    def __init__(self, inicio, fim, passo):
        self.atual = inicio
        self.fim = fim
        self.passo = passo

    def __iter__(self):
        return self

    def __next__(self):
        if self.atual >= self.fim:
            raise StopIteration
        valor = self.atual
        self.atual += self.passo
        return valor

# Teste
for i in MeuRange(0, 10, 2):
    print(i)  # 0, 2, 4, 6, 8

4. Iteradores com Estado: Mantendo e Resetando o Progresso

Iteradores com estado podem ser úteis, mas exigem cuidados especiais. Cada chamada a __iter__ deve criar um novo iterador para permitir reinícios.

class LeitorArquivoSimulado:
    def __init__(self, linhas):
        self.linhas = linhas

    def __iter__(self):
        # Cria um novo iterador a cada chamada
        return IteradorLinha(self.linhas)

class IteradorLinha:
    def __init__(self, linhas):
        self.linhas = linhas
        self.posicao = 0

    def __iter__(self):
        return self

    def __next__(self):
        if self.posicao >= len(self.linhas):
            raise StopIteration
        linha = self.linhas[self.posicao]
        self.posicao += 1
        return linha

# Demonstração de reinício
dados = ["linha1", "linha2", "linha3"]
leitor = LeitorArquivoSimulado(dados)

print("Primeira iteração:")
for linha in leitor:
    print(f"  {linha}")

print("Segunda iteração (reinicia corretamente):")
for linha in leitor:
    print(f"  {linha}")

5. Iteradores Infinitos e Controle de Parada

Iteradores infinitos são poderosos, mas exigem controle explícito para evitar loops infinitos acidentais.

class FibonacciInfinito:
    def __init__(self):
        self.a, self.b = 0, 1

    def __iter__(self):
        return self

    def __next__(self):
        valor = self.a
        self.a, self.b = self.b, self.a + self.b
        return valor

# Uso controlado com limite
fib = FibonacciInfinito()
contador = 0
for numero in fib:
    if contador >= 10:
        break
    print(numero, end=" ")  # 0 1 1 2 3 5 8 13 21 34
    contador += 1

6. Iteradores vs. Geradores: Quando Usar Cada Um

Geradores oferecem uma sintaxe mais concisa para criar iteradores:

# Versão com gerador
def gerador_fibonacci(limite):
    a, b = 0, 1
    for _ in range(limite):
        yield a
        a, b = b, a + b

# Versão com classe iteradora
class ClasseFibonacci:
    def __init__(self, limite):
        self.limite = limite
        self.a, self.b = 0, 1
        self.contador = 0

    def __iter__(self):
        return self

    def __next__(self):
        if self.contador >= self.limite:
            raise StopIteration
        valor = self.a
        self.a, self.b = self.b, self.a + self.b
        self.contador += 1
        return valor

# Ambos produzem o mesmo resultado
print(list(gerador_fibonacci(5)))    # [0, 1, 1, 2, 3]
print(list(ClasseFibonacci(5)))      # [0, 1, 1, 2, 3]

Use geradores para simplicidade e classes iteradoras quando precisar de controle de estado complexo ou herança.

7. Boas Práticas e Armadilhas Comuns

Erro comum 1: Esquecer StopIteration

class IteradorQuebrado:
    def __next__(self):
        return 42  # Nunca levanta StopIteration - loop infinito!

Erro comum 2: Confundir __iter__ com __getitem__

class ApenasIteravel:
    def __getitem__(self, index):
        if index >= 5:
            raise IndexError
        return index * 2

# Funciona com for, mas não é um iterador oficial
for item in ApenasIteravel():
    print(item)  # 0, 2, 4, 6, 8

Dica de desempenho: Sempre prefira iteradores a listas quando processar grandes volumes de dados, pois iteradores não armazenam toda a sequência em memória.

8. Iteradores na Biblioteca Padrão: Exemplos Reais

Python fornece funções embutidas e módulos que trabalham diretamente com iteradores:

# Funções embutidas iter() e next()
lista = [10, 20, 30]
iterador = iter(lista)
print(next(iterador))  # 10
print(next(iterador))  # 20

# Módulo itertools
from itertools import count, cycle, islice

# count - iterador infinito
for i in islice(count(10, 5), 5):
    print(i, end=" ")  # 10 15 20 25 30

# cycle - itera infinitamente sobre uma sequência
cores = cycle(["vermelho", "verde", "azul"])
for _ in range(6):
    print(next(cores), end=" ")  # vermelho verde azul vermelho verde azul

# Depuração com list() e tuple()
iterador_teste = iter([1, 2, 3, 4, 5])
print(list(iterador_teste))  # [1, 2, 3, 4, 5]

Referências