Entendendo iteráveis e iteradores em Python
5
0

Entendendo iteráveis e iteradores em Python

O que são iteráveis e iteradores no mundo Python? Vamos responder essas e mais outras perguntas nesse blogpost.

Marcus Gabriel
5 min
5
0
Email image

Iteração é fundamental para o processamento de grandes quantidades de dados. Imagine que você precisa processar uma quantidade X de livros com o objetivo de contar o número de repetições de cada letra presente nos textos dos livros. Dependendo da quantidade e do tamanho dos livros, certamente seria inviável carrega-los na memória do computador. Nesse caso, quando queremos percorrer conjuntos de dados que não cabem na memória, precisamos ter uma forma de acessar os itens do conjunto de modo lazy (preguiçoso), isto é, um item de cada vez e por demanda, assim, não sobrecarregamos a memória do computador com a quantidade excessiva de dados vinda do conjunto de livros.

Iterável

Em poucas palavras. Iterável é qualquer objeto que a partir da função embutida iter pode-se ser obtido um iterador. Sendo assim, qualquer objeto que implemente o método __iter__ que devolva um iterador é iterável.

Pegando um exemplo simples: imagine um laço for que percorre uma string "python" imprimindo na tela letra por letra. A string "python" é iteravel nesse caso. Quando utilizamos o laço for, um "iterador invisível" é criado para possibilitar que a iteração ocorra. O interpretador Python abstrai todo esse processo de criar o "iterador invisível" para nós. Mais a diante veremos como o interpretador cria o iterador a partida da minha_string.

minha_string = "python"
for letra in minha_string:
print(letra)
# p
# y
# t
# h
# o
# n

Para ficar mais claro, vamos imaginar que o laço for não exista, impossibilitando a implementação vista no exemplo anterior. Para simular o laço for podemos usar um laço while que itera pelo iterador criado a partir da minha_string. veja a seguir.

minha_string = "python"
meu_iterador = iter(minha_string) # 1
while True:
try:
print(next(meu_iterador)) # 2
except StopIteration: # 3
del meu_iterador # 4
break # 5
# p
# y
# t
# h
# o
# n
  1. Criamos o iterador meu_iterador a partir do iterável minha_string.
  2. Chamamos next no meu_iterator para obter o proximo item.
  3. A exception StopIteration é levantada quando não há mais itens para serem devolvidos. No exemplo anterior, StopIteration informa para o laço for que o iterável se esgotou.
  4. Liberamos a referencia a meu_iterador. Assim o objeto é descartado.
  5. Força a saída do laço.
Funcionamento do laço for.
Funcionamento do laço for.

Desmembrando o exemplo do laço for, temos os seguinte: primeiro é feito o iter(minha_string), em seguida o laço for pede o next() do iterador que foi gerado no passo 1, em seguida o laço for faz o que estivar no corpo da função, no nosso caso, print(letra) e por fim, quando o StopIteration é levantado o laço é encerrado. Veja que essa descrição bate com a implementação usando o laço while.

class IteravelPalavras:
def __init__(self, palavras):
self.palavras = palavras
def __getitem__(self, index):
return self.palavras[index]
palavras = ["python", "javascript", "elixir", "rust"]
iteravel_palavras = IteravelPalavras(palavras)
for palavra in iteravel_palavras:
print(palavra)
# python
# javascript
# elixir
# rust

No exemplo acima, implementamos um iterável chamado IteravelPalavras que armazena um lista de palavras. Segundo a definição de iterável se eu chamar iter(iteravel) ele deve me retornar um iterador. Com o iterador em mãos, eu consigo iterar sobre ele e fazer oque quiser com o valores retornados. No nosso exemplo, utilizamos o laço for para conseguir um iterador a partir do iteralve_palavras , que é do tipo IteravelPalavras, e em seguida imprimimos as palavras armazenadas nele. Lembre-se que o laço for utiliza a função embutida iter() automaticamente para gerar o iterador como vimos anteriormente.

Para concluir. Sempre que o interpretador Python precisa iterar por um objeto MeuObjeto, ele chama iter(MeuObjeto). A função embutida iter, segue os seguintes passos: Primeiro, ela verifica se o objeto MeuObjeto implementa o método __iter__ e o chama para obter o iterador. Segundo, caso o método __iter__ não esteja implementado, mas o método __getitem__ esteja implementado, o interpretador Python cria um iterador que tentará acessar os itens em sequencia, começando sempre pelo índice 0 (zero). Terceiro, se os dois passos anteriores falharem o interpretador Python levantará TypeError, normalmente informando: "TypeError: Obj object is not iterable", onde Obj é a classe do objeto MeuObjeto. Note que em nosso exemplo, implementamos apenas o método __getitem__ e o interpretador Python conseguiu criar um iterador a partir dele.

Iterador

É considerado interador qualquer objeto que implemente o método __next__, sem argumentos, retornando o proximo item ou levantando a exception StopIteration quando não houver mais itens. Mas veja: Iteradores também implementam o método __iter__, portanto também são iteráveis.

No livro da GoF, Design Patterns: Elements of Reusable Object-Oriented Software, o padrão Iterator tem como objetivo "Fornecer um meio de acessar, sequencialmente, os elementos de um objeto agregado sem expor a sua representação subjacente." Vamos ver como ficaria a implementação , em python, segundo o livro da GoF.

Um implementação possível do padrão Iter segundo o livro da GoF.
Um implementação possível do padrão Iter segundo o livro da GoF.
class IteravelPalavras:
def __init__(self, palavras):
self.palavras = palavras
def __iter__(self):
return IteradorPalavras(self.palavras)
class IteradorPalavras:
def __init__(self, palavras):
self.palavras = palavras
self.index = 0
def __next__(self):
try:
palavra = self.palavras[self.index]
except IndexError:
raise StopIteration()
self.index += 1
return palavra
def __iter__(self):
return self
palavras = ["python", "javascript", "elixir", "rust"]
iteravel_palavras = IteravelPalavras(palavras)
for palavra in iteravel_palavras:
print(palavra)
# python
# javascript
# elixir
# rust

Note que o exemplo acima é o mesmo do exemplo da seção Iterável. Mas agora seguindo a definição do livro da GoF. Substituímos o método __getitem__ pelo método __iter__ propositalmente para enfatizar que __iter__ atende o protocolo de iteráveis ao instanciar e devolver im iterador. Quando olhamos para IteradorPalavras, ele é o iterador, implementando o método __next__, sem parâmetro, retornando o próximo item da sequencia e levantando StopIteration quando os itens se esgotam. E já que todo iterador é iterável, também é implementado o método __iter__, que retorna ele mesmo, já que ele mesmo é o iterador.

Agora vamos ver uma maneira mais pythônica de implementar esse iterador.

class IteravelPalavras:
def __init__(self, palavras):
self.palavras = palavras
def __iter__(self):
for palavra in self.palavras:
yield palavra
palavras = ["python", "javascript", "elixir", "rust"]
iteravel_palavras = IteravelPalavras(palavras)
for palavra in iteravel_palavras:
print(palavra)
# python
# javascript
# elixir
# rust

Aqui, novamente temos uma implementação diferente de IteravelPalavras, que funciona da mesma forma que os exemplos anteriores. A diferença aqui é que estamos utilizando um função geradora, deixando o código mais pythônico.

Próximos passos

Bom, vimos o que são, como funcionam e para que servem os objetos iteráveis e iteradores. Também vimos vários exemplos em código e imagens sobre o assunto. Para os próximos passos recomendo pesquisar por geradores, funções geradores e yield, esse seria o assunto que vem a seguir. Provavelmente escreverei outro blogpost abordando esse assunto nas próximas semanas e quando o fizer irei referenciar aqui nesse blogpost.

Convido todos e todas para deixar feedbacks, duvidas, dicas ou qualquer outra coisa que venha acrescentar.

Para acessar os exemplos em código acesse esse repositório no Github. Até mais.