Iteradores em Python
LidoNesta aula, você vai dominar o protocolo de iteração em Python, entendendo como objetos se tornam iteráveis ou iteradores. Vamos explorar as funções iter() e next(), criar nossos próprios iteráveis e lidar com a exceção StopIteration, com exemplos práticos e exercícios.
Iteradores são um dos conceitos fundamentais do Python, pois estão por trás de laços for, compreensões de lista e de toda a iteração sobre coleções. Nesta aula, vamos desvendar o protocolo de iteração, que define como objetos podem ser percorridos, e aprender a criar nossos próprios objetos iteráveis.
Vamos começar entendendo a diferença entre objetos iteráveis e iteradores, depois explorar as funções iter() e next(), criar nossas próprias classes iteráveis e, finalmente, dominar o tratamento da exceção StopIteration, que sinaliza o fim da iteração. Ao final, você terá uma base sólida para trabalhar com streams, geradores e pipelines de dados.
Protocolo de iteração
O protocolo de iteração em Python é composto por dois métodos especiais: __iter__() e __next__(). Um objeto é considerado iterável se implementar __iter__(), que deve retornar um objeto iterador. O iterador, por sua vez, implementa __next__(), que retorna o próximo elemento da sequência, ou levanta StopIteration quando não há mais elementos.
Essa distinção é importante: todo iterador é iterável (pois geralmente implementa __iter__() retornando a si mesmo), mas nem todo iterável é um iterador. Por exemplo, uma lista é iterável, mas não é um iterador: quando você chama iter(lista), obtém um iterador separado que percorre a lista. Isso permite que você percorra a lista várias vezes de forma independente, enquanto um iterador é consumido após uma passagem.
Vejamos um exemplo simples de uma classe que implementa ambos os métodos e, portanto, é um iterador e um iterável:
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:
self.atual += 1
return self.atual - 1
else:
raise StopIteration
# Testando
for numero in Contador(3):
print(numero)
Quando você executa o laço for, o Python chama iter() no objeto Contador(3), que retorna o próprio objeto (porque __iter__ retorna self). Em seguida, chama next() repetidamente até que StopIteration seja levantada. Esse é o mecanismo fundamental por trás de qualquer iteração.
iter e next
As funções embutidas iter() e next() são as portas de entrada para o protocolo. iter(obj) chama o método __iter__ do objeto e retorna o iterador correspondente. Se o objeto não for iterável, levanta TypeError. next(iterator) chama __next__ e retorna o próximo item, ou levanta StopIteration se o iterador estiver esgotado.
Essas funções também aceitam um valor padrão opcional: next(iterator, default) retorna default em vez de levantar StopIteration quando o iterador se esgota. Isso é útil para evitar exceções quando você não tem certeza se ainda há elementos.
Vejamos exemplos práticos:
# Obtendo um iterador a partir de uma lista
lista = [10, 20, 30]
it = iter(lista)
print(next(it)) # 10
print(next(it)) # 20
print(next(it)) # 30
# print(next(it)) # StopIteration (descomente para ver)
# Usando valor padrão
it2 = iter(lista)
print(next(it2, 'fim')) # 10
print(next(it2, 'fim')) # 20
print(next(it2, 'fim')) # 30
print(next(it2, 'fim')) # fim (não levanta exceção)
A função iter() também pode ser usada com uma função sentinela: iter(callable, sentinel). Nesse caso, a função callable é chamada repetidamente até que retorne o valor sentinel, momento em que o iterador é encerrado. Isso é útil para ler linhas de um arquivo até um marcador, por exemplo:
# Lendo até a linha 'fim'
linhas = []
with open('dados.txt', 'r') as arquivo:
for linha in iter(lambda: arquivo.readline().strip(), 'fim'):
linhas.append(linha)
Dominar iter() e next() é essencial para entender como o Python itera internamente e para criar funções que consomem iteradores de forma eficiente.
Criando iteráveis
Para criar seus próprios objetos iteráveis, você pode implementar a interface __iter__ e __next__ em uma classe, como fizemos no primeiro exemplo. Porém, muitas vezes é mais conveniente usar geradores, que são funções que produzem uma sequência de valores sob demanda usando yield. Geradores são uma forma elegante de criar iteradores sem escrever uma classe completa.
Além disso, você pode criar iteráveis que não são iteradores: a classe implementa __iter__ retornando um objeto iterador separado, geralmente definido em uma classe auxiliar. Isso permite que o objeto seja percorrido várias vezes, pois cada chamada a iter() retorna um novo iterador.
Vejamos um exemplo de um iterável que gera a sequência de Fibonacci:
class Fibonacci:
def __init__(self, quantidade):
self.quantidade = quantidade
def __iter__(self):
return FibonacciIterator(self.quantidade)
class FibonacciIterator:
def __init__(self, quantidade):
self.quantidade = quantidade
self.a, self.b = 0, 1
self.contador = 0
def __next__(self):
if self.contador >= self.quantidade:
raise StopIteration
self.contador += 1
valor = self.a
self.a, self.b = self.b, self.a + self.b
return valor
fib = Fibonacci(5)
print(list(fib)) # [0, 1, 1, 2, 3]
print(list(fib)) # [0, 1, 1, 2, 3] (pode iterar novamente)
Note que a classe Fibonacci não implementa __next__, apenas __iter__; ela é iterável, mas não é um iterador. Cada chamada a iter(fib) cria um novo FibonacciIterator, permitindo múltiplas iterações independentes.
Usando geradores, o mesmo conceito fica muito mais conciso:
def fibonacci(quantidade):
a, b = 0, 1
for _ in range(quantidade):
yield a
a, b = b, a + b
fib = fibonacci(5)
print(list(fib)) # [0, 1, 1, 2, 3]
Geradores são a forma recomendada para criar iteradores simples, pois são mais legíveis e menos propensos a erros. No entanto, entender a implementação com classes é fundamental para compreender o protocolo subjacente.
StopIteration
A exceção StopIteration é usada para sinalizar o fim da iteração. Quando um iterador não tem mais itens para retornar, seu método __next__ deve levantá-la. O laço for captura essa exceção silenciosamente e termina o loop. Se você chamar next() manualmente e a exceção for levantada, ela se propaga, a menos que você forneça um valor padrão.
Em Python 3.7+, StopIteration dentro de um gerador é transformada em RuntimeError se não for capturada, para evitar bugs sutis. Isso significa que você não deve lançar StopIteration manualmente dentro de um gerador; em vez disso, use return para encerrar a geração.
Vejamos um exemplo de tratamento manual:
it = iter([1, 2, 3])
while True:
try:
item = next(it)
print(item)
except StopIteration:
print("Fim da iteração")
break
Você também pode usar StopIteration para controlar a lógica de funções que consomem iteradores, como na implementação da função reduce ou em algoritmos que precisam saber quando parar. É importante lembrar que StopIteration é uma exceção normal, não um erro: ela é usada para controle de fluxo no protocolo de iteração.
Quando você cria um iterador personalizado, certifique-se de que __next__ levante StopIteration exatamente quando não houver mais itens, e não antes nem depois, para evitar loops infinitos ou itens faltantes.
Boas práticas e observações finais
Algumas recomendações ao trabalhar com iteradores:
- Prefira geradores a classes para iteradores simples, pois são mais concisos e legíveis.
- Se você precisa de um iterável que possa ser percorrido várias vezes, implemente
__iter__retornando um novo iterador a cada chamada, em vez de retornarself. - Use o valor padrão de
next()para evitar exceções quando você não tem certeza se o iterador está esgotado. - Não armazene iteradores em estruturas de dados se você precisar percorrê-los mais de uma vez; eles são consumíveis.
- Lembre-se de que
StopIterationé uma exceção de controle, mas em geradores, prefirareturnaraise StopIteration.
Com esses conceitos, você está pronto para explorar tópicos avançados como itertools, geradores infinitos e pipelines de dados.
Referências
- Documentação Python – Tipos de Iteradores
- Documentação Python – função iter()
- Documentação Python – função next()
- Documentação Python – Expressões geradoras
- Real Python – Iterators and Iterables
- Documentação Python – StopIteration
Exercícios
- Exercício 1: Implemente uma classe chamada
Paresque seja um iterador que gera os primeirosnnúmeros pares (0, 2, 4, ...). A classe deve recebernno construtor. Teste com um loop for. - Exercício 2: Usando a função
iter()com sentinela, leia linhas do teclado (input) até que o usuário digite "fim". Armazene as linhas digitadas (sem incluir "fim") em uma lista e imprima. - Exercício 3: Crie um iterável chamado
ContagemRegressivaque possa ser percorrido várias vezes. Ele deve gerar números de um valor inicial até 0. Use uma classe auxiliar para o iterador. Teste percorrendo duas vezes. - Exercício 4: Escreva um gerador chamado
quadradosque receba um númerone gere os quadrados de 1 até n (1², 2², ...). Use um loop for para imprimir os valores. - Exercício 5: Implemente um iterador que leia de um arquivo linha por linha, mas que levante
StopIterationquando encontrar uma linha vazia (ou seja, pare no primeiro vazio). Use a classeArquivoLinhasque recebe o caminho do arquivo. Teste com um arquivo de exemplo.
class Pares:
def __init__(self, n):
self.n = n
self.atual = 0
self.contador = 0
def __iter__(self):
return self
def __next__(self):
if self.contador >= self.n:
raise StopIteration
self.contador += 1
valor = self.atual
self.atual += 2
return valor
# Teste
for p in Pares(5):
print(p) # 0, 2, 4, 6, 8
linhas = []
print("Digite linhas (ou 'fim' para encerrar):")
for linha in iter(lambda: input(), 'fim'):
linhas.append(linha)
print("Linhas digitadas:")
for l in linhas:
print(l)
class ContagemRegressiva:
def __init__(self, inicio):
self.inicio = inicio
def __iter__(self):
return ContagemIterator(self.inicio)
class ContagemIterator:
def __init__(self, inicio):
self.atual = inicio
def __next__(self):
if self.atual < 0:
raise StopIteration
valor = self.atual
self.atual -= 1
return valor
# Teste
c = ContagemRegressiva(3)
print(list(c)) # [3, 2, 1, 0]
print(list(c)) # [3, 2, 1, 0] (novamente)
def quadrados(n):
for i in range(1, n+1):
yield i*i
for q in quadrados(5):
print(q) # 1, 4, 9, 16, 25
class ArquivoLinhas:
def __init__(self, caminho):
self.caminho = caminho
def __iter__(self):
self.arquivo = open(self.caminho, 'r')
return self
def __next__(self):
linha = self.arquivo.readline()
if linha == '':
self.arquivo.close()
raise StopIteration
linha = linha.rstrip('\n')
if linha == '':
self.arquivo.close()
raise StopIteration
return linha
# Supondo que 'dados.txt' tenha:
# linha1
# linha2
#
# linha4
for l in ArquivoLinhas('dados.txt'):
print(l) # imprime linha1, linha2 e para antes da linha4