Python Iluminado

Iteradores

Iteradores são objetos que podem ser iterados. Nesse guia vamos estudá-los e aprender como eles funcionam construindo um iterador usando os métodos __iter__ e __next__.

  • Um objeto iterável é um objeto que implementa __iter__, que deve retornar um objeto iterador.
  • Um iterador é um objeto que implementa next, que deve retornar o próximo elemento do objeto iterável que o retornou e gerar uma exceção StopIteration quando não houver mais elementos disponíveis.

Introdução

Iteradores sempre estiveram conosco em Python, eles estão implementados nos for loops, geradores, etc, porém estão "escondidos". Um iterador é simplesmente um objeto que podemos iterar nele, ou seja, um objeto que retornará dados, um elemento de cada vez.

O objeto iterador implementa dois métodos especiais __iter__() e __next__(), que são conhecidos como o protocolo do iterador. Um objeto é chamado de iterável se formos capazes de obter um iterador dele, estruturas como lista, tupla e string são iteráveis. A função iter() (que chama o método __iter__) retorna um iterador delas.

Vamos então aos experimentos para entendermos melhor os iteradores, usaremos a função next() para iterar manualmente através de todos os itens de um iterador. Quando chegarmos ao fim e não houver mais dados para retorno, ocorrerá um erro StopIteration.

lista = [1,2,3,4]
iterador = iter(lista)
print(next(iterador)) # 1
print(next(iterador)) # 2
print(iterador.__next__()) # 3
print(iterador.__next__()) # 4
next(iterador) # Sem mais itens para iterar, erro StopIteration ocorre

Uma forma melhor e mais simples de iterar automaticamente seria usando um for loop. Usando ele, nós podemos iterar sob qualquer objeto que retorne um iterador, por exemplo uma lista, string, arquivo, etc.

for elemento in lista:
print(lista)
# 1
# 2
# 3
# 4

Vimos que o for foi capaz de iterar automaticamente através da lista, agora vamos analisar como ele faz essa mágica internamente no Python:

for elemento in iteravel:
# faça algo com o elemento

É na verdade:

# cria um objeto iterador do iteravel
objeto_iteravel = iter(iteravel)
# Loop infinito
while True:
try:
# obtém o novo item
elemento = next(objeto_iteravel)
# faz algo com o elemento
except StopIteration:
# Se o StopIteration ocorrer, break no loop
break

Veja que internamente o for loop cria um objeto iterador objeto_iteravel chamando o método iter() nele, e que na verdade ele é um while loop. Dentro desse while loop ele chama next() para pegar o próximo elemento e executar o for loop com esse valor, assim que todos os itens forem percorridos, StopIteration é acionado que é capturado internamente e o loop acaba, perceba que qualquer outra exceção irá passar.

Criando um Iterador

Para criar um objeto/classe como um iterador nós precisamos implementar os métodos __iter__() e __next__() no nosso objeto. Como aprendemos no nosso capítulo de Classes e Objetos, todas as classes possuem uma função chamada __init__(), que possibilita a inicialização quando o objeto é criado. O método __iter__() age similar, pode executar operações (inicializações, etc), mas sempre deve retornar o objeto iterador. O método __next__() também possibilita operações e deve retornar o próximo item na sequência.

class Numeros:
def __iter__(self):
self.x = 1
return self
def __next__(self):
y = self.x
self.x += 1
return y
n = Numeros()
iterador = iter(n)
print(next(iterador))
print(next(iterador))
print(next(iterador))
print(next(iterador))
print(next(iterador))
# 1
# 2
# 3
# 4
# 5

StopIteration

O exemplo acima pode ser executado infinitamente, especialmente se aplicarmos um for loop. Para previnirmos que a iteração ocorra eternamente, podemos usar o comando StopIteration no método __next__(), nós podemos adicionar a condição de término para que o erro seja disparado caso a iteração ultrapasse a condição.

class Numeros:
def __iter__(self):
self.x = 1
return self
def __next__(self):
if self.x <= 6:
y = self.x
self.x +=1
return y
else:
raise StopIteration
n = Numeros()
iterador = iter(n)
for elemento in iterador:
print(elemento)
# 1
# 2
# 3
# 4
# 5
# 6

Iterador Aleatório

Vejamos um iterador que nos retorna sequências de comprimento aleatórios de 1's.

import random
class RandomIterable:
def __iter__(self):
return self
def __next__(self):
if random.choice(["go", "go", "stop"]) == "stop":
raise StopIteration # finaliza o iterador
return 1
for x in RandomIterable():
print(x)
# 1
# 1
# 1

Esses foram os iteradores, conceito importante e muito presente em nossos estudos, central na linguagem Python, vamos então continuar iterando nosso conhecimento.