Se você já usou um loop for em Python, você já usou iteradores. Mas você sabe o que acontece nos bastidores? Os iteradores são um dos pilares da linguagem Python, permitindo que estruturas de dados como listas, tuplas, dicionários e conjuntos sejam percorridas de forma uniforme e eficiente. Entender como eles funcionam é essencial para escrever código Python idiomático e performático.

Neste guia completo sobre iteradores em Python, você vai aprender desde o protocolo de iteração até a criação de iteradores personalizados, explorando o poderoso módulo itertools e entendendo as diferenças cruciais entre iteradores e geradores. Cada conceito é acompanhado de exemplos práticos para fixação.

O Que São Iteradores em Python?

Um iterador é um objeto que implementa o protocolo de iteração, composto por dois métodos: __iter__() e __next__(). O método __iter__() retorna o próprio objeto iterador, enquanto __next__() retorna o próximo valor da sequência. Quando não há mais valores, __next__() levanta a exceção StopIteration para sinalizar o fim da iteração.

Na prática, isso significa que qualquer objeto que possa ser percorrido com um loop for é, de alguma forma, um iterável que produz um iterador. O loop for em Python é, na verdade, um açúcar sintático que chama iter() no objeto e depois next() repetidamente até encontrar StopIteration.

# O que acontece internamente quando você faz:
for item in [1, 2, 3]:
    print(item)

# É equivalente a:
iterador = iter([1, 2, 3])  # Chama __iter__() internamente
while True:
    try:
        item = next(iterador)  # Chama __next__() internamente
        print(item)
    except StopIteration:
        break

Fonte: Documentação Oficial Python - Iterators

Iteráveis vs Iteradores: Qual a Diferença?

Essa é uma das dúvidas mais comuns entre desenvolvedores Python. Embora relacionados, iteráveis e iteradores são conceitos distintos:

  • Iterável: É todo objeto que pode ser iterado, ou seja, que implementa o método __iter__() (ou __getitem__()). Listas, tuplas, strings, dicionários e conjuntos são exemplos de iteráveis. Um iterável pode ser usado em um loop for e pode ser convertido em um iterador com a função iter().
  • Iterador: É um objeto que implementa ambos __iter__() e __next__(). Todo iterador é um iterável (porque implementa __iter__()), mas nem todo iterável é um iterador.

A diferença prática mais importante é que um iterador só pode ser percorrido uma vez. Depois que todos os elementos são consumidos, o iterador se esgota e não pode ser reiniciado. Já um iterável pode gerar novos iteradores quantas vezes você quiser.

lista = [1, 2, 3]  # lista é um iterável, NÃO é um iterador

iterador1 = iter(lista)  # iterador1 é um iterador
iterador2 = iter(lista)  # iterador2 é um novo iterador independente

print(next(iterador1))  # 1
print(next(iterador1))  # 2
print(next(iterador1))  # 3
# print(next(iterador1))  # StopIteration - iterador esgotado

print(next(iterador2))  # 1 - iterador2 ainda está no início

Fonte: Glossário Python - Iterable

O Protocolo de Iteração em Detalhe

O protocolo de iteração é a base de todo o sistema de iteração em Python. Vamos detalhar cada parte:

O Método __iter__()

Este método deve retornar um objeto iterador. Em iteráveis como listas, ele retorna um objeto iterador especializado. Em iteradores, ele simplesmente retorna self (o próprio objeto).

O Método __next__()

Este método deve retornar o próximo elemento da sequência. Quando não há mais elementos, deve levantar StopIteration. É isso que sinaliza ao loop for que a iteração terminou.

A Exceção StopIteration

O StopIteration é a forma que o Python usa para comunicar que um iterador foi consumido por completo. Embora você possa usá-la em seu código, é mais comum lidar com ela indiretamente através de loops for.

Fonte: PEP 234 - Iterators

Criando Iteradores Personalizados

Criar seus próprios iteradores é uma habilidade valiosa em Python. Você pode encapsular lógicas de iteração complexas em classes limpas e reutilizáveis. Vamos ver como fazer isso na prática.

Iterador de Números Pares

class Pares:
    """Iterador que retorna números pares até um limite."""

    def __init__(self, limite):
        self.limite = limite
        self.valor = 0

    def __iter__(self):
        return self

    def __next__(self):
        if self.valor > self.limite:
            raise StopIteration
        resultado = self.valor
        self.valor += 2
        return resultado

# Usando o iterador personalizado
for par in Pares(10):
    print(par)  # 0, 2, 4, 6, 8, 10

Iterador de Fibonacci

class Fibonacci:
    """Iterador que gera a sequência de Fibonacci."""

    def __init__(self, maximo):
        self.maximo = maximo
        self.a, self.b = 0, 1
        self.contador = 0

    def __iter__(self):
        return self

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

fib = Fibonacci(10)
print(list(fib))  # [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

Iterador com Estado Resetável

Diferente de um iterador puro, podemos criar classes que são iteráveis (não iteradores) e produzem um novo iterador a cada chamada de iter(), permitindo que sejam percorridas múltiplas vezes:

class PalavrasTexto:
    """Iterável que produz um novo iterador para cada iteração."""

    def __init__(self, texto):
        self.texto = texto

    def __iter__(self):
        return PalavrasIterator(self.texto)

class PalavrasIterator:
    def __init__(self, texto):
        self.palavras = texto.split()
        self.indice = 0

    def __iter__(self):
        return self

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

frase = PalavrasTexto("Python é uma linguagem poderosa")
for p in frase:
    print(p)  # Python, é, uma, linguagem, poderosa

# Pode percorrer novamente
for p in frase:
    print(p.upper())  # PYTHON, É, UMA, LINGUAGEM, PODEROSA

Saiba mais sobre métodos mágicos como __iter__ e __next__ lendo nosso guia completo sobre métodos mágicos em Python.

Fonte: Real Python - Python Iterators and Iterables

Iteradores em Estruturas de Dados Nativas

Cada estrutura de dados em Python implementa a iteração de forma específica. Vamos explorar como funcionam os iteradores para os tipos mais comuns:

Listas e Tuplas

Produzem iteradores que percorrem os elementos na ordem em que foram inseridos. O iterador da lista mantém um índice interno que avança a cada chamada de next().

lista = [10, 20, 30]
it = iter(lista)
print(next(it))  # 10
print(next(it))  # 20

Dicionários

Por padrão, iterar sobre um dicionário percorre suas chaves. Você pode usar .values() para valores e .items() para pares chave-valor.

d = {'a': 1, 'b': 2, 'c': 3}
for chave in d:           # Chaves
    print(chave)
for valor in d.values():   # Valores
    print(valor)
for k, v in d.items():     # Chave e valor
    print(f"{k}: {v}")

Strings

Strings são iteráveis e produzem um iterador que percorre cada caractere individualmente, incluindo espaços e caracteres especiais.

for char in "Python":
    print(char)  # P, y, t, h, o, n

Arquivos

Arquivos são iteráveis em Python! Cada chamada de next() retorna uma linha do arquivo, tornando a leitura de arquivos grandes extremamente eficiente em memória.

with open("dados.txt", "r") as arquivo:
    for linha in arquivo:  # Lê uma linha por vez
        print(linha.strip())

Fonte: Python Wiki - Iterator

Trabalhando com itertools

O módulo itertools é uma biblioteca padrão do Python que fornece funções para criar iteradores avançados. É uma ferramenta indispensável para qualquer desenvolvedor Python que trabalha com iteração. Vamos explorar as funções mais úteis:

count() - Contador Infinito

from itertools import count

for i in count(5):  # 5, 6, 7, 8, 9, ...
    if i > 10:
        break
    print(i)

cycle() - Ciclo Infinito

from itertools import cycle

cores = cycle(["vermelho", "azul", "verde"])
for _ in range(6):
    print(next(cores))  # vermelho, azul, verde, vermelho, azul, verde

chain() - Encadeando Iteradores

from itertools import chain

lista1 = [1, 2, 3]
lista2 = [4, 5, 6]
for item in chain(lista1, lista2):
    print(item)  # 1, 2, 3, 4, 5, 6

islice() - Fatiamento de Iteradores

from itertools import islice

# Pega os primeiros 5 elementos de um contador infinito
for i in islice(count(10), 5):
    print(i)  # 10, 11, 12, 13, 14

product(), permutations(), combinations()

Essas funções geram iteradores matemáticos poderosos para produto cartesiano, permutações e combinações:

from itertools import product, permutations, combinations

# Produto cartesiano
print(list(product([1, 2], ["a", "b"])))  
# [(1, 'a'), (1, 'b'), (2, 'a'), (2, 'b')]

# Permutações
print(list(permutations([1, 2, 3], 2)))
# [(1, 2), (1, 3), (2, 1), (2, 3), (3, 1), (3, 2)]

# Combinações
print(list(combinations([1, 2, 3], 2)))
# [(1, 2), (1, 3), (2, 3)]

Fonte: Documentação Oficial - itertools

Iteradores vs Geradores: Quando Usar Cada Um

Esta é uma pergunta frequente em entrevistas técnicas e na prática diária. Tanto iteradores quanto geradores servem para produzir sequências de dados sob demanda, mas cada um tem seu lugar.

Geradores (que usam yield) são uma forma simplificada de criar iteradores. Enquanto um iterador personalizado exige uma classe com os métodos __iter__() e __next__(), um gerador é escrito como uma função normal usando yield. Para a maioria dos casos, geradores são a escolha mais prática.

Iteradores personalizados são mais adequados quando você precisa de:

  • Manter estado complexo entre iterações
  • Implementar múltiplos métodos além da iteração
  • Criar uma abstração rica com comportamento específico
  • Reinicialização ou reset do estado (através de um iterável separado do iterador)
# Quando usar gerador (mais simples):
def quadrados(n):
    for i in range(n):
        yield i ** 2

# Quando usar iterador personalizado (mais controle):
class Quadrados:
    def __init__(self, n, prefixo=""):
        self.n = n
        self.i = 0
        self.prefixo = prefixo

    def __iter__(self):
        return self

    def __next__(self):
        if self.i >= self.n:
            raise StopIteration
        resultado = f"{self.prefixo}{self.i ** 2}"
        self.i += 1
        return resultado

    def reset(self):
        self.i = 0

Confira também nosso guia completo sobre Python Generators para entender a fundo essa alternativa poderosa aos iteradores.

Fonte: GeeksforGeeks - Iterators in Python

Funções Built-in que Trabalham com Iteradores

Python oferece várias funções nativas que operam diretamente com iteradores:

numeros = [1, 2, 3, 4, 5]

# map - aplica função a cada elemento
dobrados = map(lambda x: x * 2, numeros)
print(list(dobrados))  # [2, 4, 6, 8, 10]

# filter - filtra elementos
pares = filter(lambda x: x % 2 == 0, numeros)
print(list(pares))  # [2, 4]

# zip - agrupa elementos de múltiplos iteráveis
nomes = ["Ana", "Bob", "Eve"]
idades = [25, 30, 28]
for nome, idade in zip(nomes, idades):
    print(f"{nome} tem {idade} anos")

# enumerate - enumera elementos com índice
for i, nome in enumerate(nomes, start=1):
    print(f"{i}: {nome}")

# reversed - itera na ordem inversa
for num in reversed([1, 2, 3]):
    print(num)  # 3, 2, 1

# sorted - itera ordenadamente
for num in sorted([3, 1, 2]):
    print(num)  # 1, 2, 3

# any e all - verificam condições em iteradores
print(any(x > 3 for x in [1, 2, 3, 4]))  # True
print(all(x > 0 for x in [1, 2, 3, 4]))  # True

Fonte: Programiz - Python Iterators

Boas Práticas com Iteradores

Trabalhar com iteradores de forma eficiente requer atenção a alguns pontos importantes:

1. Consciência do Consumo Único

Lembre-se: iteradores são consumidos uma única vez. Se você precisar percorrer os dados múltiplas vezes, converta o iterador em uma lista (se a memória permitir) ou crie um iterável que produza novos iteradores.

# Problema: iterador consumido após primeiro loop
it = iter([1, 2, 3])
for i in it:
    print(i)
# Segundo loop não executa - it já foi consumido
for i in it:
    print(i)

# Solução 1: converter para lista
lista = list(iter([1, 2, 3]))

# Solução 2: criar um iterável que gera novos iteradores
class MeusNumeros:
    def __iter__(self):
        return iter([1, 2, 3])

2. Eficiência com Arquivos Grandes

Aproveite que arquivos são iteráveis para processar grandes volumes sem carregar tudo na memória:

with open("grande_arquivo.csv") as f:
    for linha in f:
        processar(linha)  # Uma linha por vez na memória

3. Generator Expressions vs List Comprehensions

Use generator expressions (com parênteses) em vez de list comprehensions (com colchetes) quando não precisar de todos os valores de uma vez. A economia de memória pode ser enorme.

# List comprehension - cria toda a lista na memória
quadrados_lista = [x**2 for x in range(1000000)]

# Generator expression - produz sob demanda
quadrados_gen = (x**2 for x in range(1000000))

4. Encadeamento de Iteradores

Você pode combinar múltiplos iteradores e funções built-in para criar pipelines de processamento elegantes e eficientes:

dados = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

pipeline = (
    x
    for x in dados
    if x % 2 == 0          # filtra pares
)

pipeline = (x * 2 for x in pipeline)  # dobra
pipeline = (f"Número: {x}" for x in pipeline)  # formata

for item in pipeline:
    print(item)
# Número: 4, Número: 8, Número: 12, Número: 16, Número: 20

Fonte: Python Functional Programming HOWTO

Conclusão

Os iteradores em Python são um conceito fundamental que está presente em praticamente todo código Python, muitas vezes sem que você perceba. Desde o simples loop for até pipelines complexos de processamento de dados, o protocolo de iteração é a espinha dorsal que torna tudo possível.

Neste guia, você aprendeu:

  • O que são iteradores e como eles se diferenciam de iteráveis
  • O protocolo de iteração com __iter__(), __next__() e StopIteration
  • Como criar iteradores personalizados com classes
  • Como usar o módulo itertools para iteradores avançados
  • As diferenças entre iteradores e geradores
  • Boas práticas para trabalhar com iteradores de forma eficiente

Dominar iteradores vai tornar seu código Python mais idiomático, eficiente e elegante. Pratique criando seus próprios iteradores e explorando o módulo itertools — são ferramentas que você usará constantemente em projetos reais.

Fonte: Documentação Oficial Python - Função iter()