Iteradores e iteráveis em Python

Você sabe o que é um iterável? E um iterador? Como reconhecer essas estruturas em Python? Responder tais questionamentos é o objetivo desse artigo.

Iteráveis

Comecemos criando uma função que recebe um conjunto de valores, os eleva ao quadrado e retorna uma lista com tais quadrados:

def eleva_ao_quadrado(iteravel_de_numeros):
    resultado = []
    for numero in iteravel_de_numeros:
        resultado.append(numero**2)
    return resultado

Observe que o parâmetro da função foi denominado iteravel_de_numeros. O objetivo é demonstrar o que pode ser iterável e como reconhecer um iterável.

Comecemos com a documentação da linguagem. Em seu glossário, um iterável é definido como:

Um objeto capaz de retornar seus membros um de cada vez. Exemplos de iteráveis incluem todos os tipos de sequência (tais como list, str e tuple) e alguns tipos não sequenciais como dict, objeto arquivo, e objetos de qualquer classe que você definir com um método __iter__() ou com um método __getitem__() que implemente a semântica de Sequence.

Iteráveis podem ser usados em um laço for e em vários outros lugares em que uma sequência é necessária (zip(), map(), …). Quando um objeto iterável é passado como argumento para a função nativa iter(), ela retorna um iterador para o objeto. Este iterador é adequado para se varrer todo o conjunto de valores. Ao usar iteráveis, normalmente não é necessário chamar iter() ou lidar com os objetos iteradores em si. A instrução for faz isso automaticamente para você, criando uma variável temporária para armazenar o iterador durante a execução do laço.

Nada melhor que um exemplo para entender essas tecnicalidades. Vamos começar criando uma variável numeros que será uma tupla de inteiros, visto que o glossário diz que tipos sequenciais são naturalmente iteráveis:

numeros = (1, 2, 3)

Observe que a documentação diz que é possível reconhecer um iterável a partir de um método __iter__() ou __getitem__(). Para verificar os atributos e métodos disponíveis para um objeto, há a função dir:

dir(numeros)
['__add__',
 '__class__',
 '__contains__',
 '__delattr__',
 '__dir__',
 '__doc__',
 '__eq__',
 '__format__',
 '__ge__',
 '__getattribute__',
 '__getitem__',
 '__getnewargs__',
 '__gt__',
 '__hash__',
 '__init__',
 '__init_subclass__',
 '__iter__',
 '__le__',
 '__len__',
 '__lt__',
 '__mul__',
 '__ne__',
 '__new__',
 '__reduce__',
 '__reduce_ex__',
 '__repr__',
 '__rmul__',
 '__setattr__',
 '__sizeof__',
 '__str__',
 '__subclasshook__',
 'count',
 'index']

Veja que tais métodos estão presentes. Caso tenha dificuldade em achar na lista, o seguinte código confirma a presença de tais métodos (e ainda te deixa curioso para pesquisar sobre set em Python 🙂

set(('__iter__', '__getitem__')) & set(dir(numeros))
{'__getitem__', '__iter__'}

OK, numeros é um iterável. Seguindo o texto do glossário, é possível passar por cada item usando um loop for. Olhe novamente o corpo da função criada no início do artigo e perceba que é examente isso que ocorre.

Assim, é possível passar numeros como argumento de nossa função:

eleva_ao_quadrado(numeros)
[1, 4, 9]

Faça de exercício numeros como sendo uma lista e veja que funciona igualmente. Crie seus próprios exercícios com os tipos citados no glossário para entender ainda mais o assunto.

Outra forma de reconhecer numeros como um iterável é através de uma list comprehension, já que nesta também ocorre um loop for:

[numero**2 for numero in numeros]
[1, 4, 9]

O glossário também diz que é possível passar um iterável em funções da linguagem como a map. A map aplica uma função a todos os itens de um iterável. Vamos criar uma função anônima que também eleva ao quadrado todos os itens:

map(lambda x : x**2, numeros)
<map at 0x7fe4d05d5d60>

Observe que map retorna um objeto na memória que, na realidade, é um iterador, nosso próximo assunto. Mas, antes, apenas para mostrar que funcionou, vamos passar o map para uma lista:

list(map(lambda x : x**2, numeros))
[1, 4, 9]

Iteradores

Vamos novamente consultar o glossário:

Um objeto que representa um fluxo de dados. Repetidas chamadas ao método __next__() de um iterador (ou passando o objeto para a função embutida next()) vão retornar itens sucessivos do fluxo. Quando não houver mais dados disponíveis uma exceção StopIteration exception será levantada. Neste ponto, o objeto iterador se esgotou e quaisquer chamadas subsequentes a seu método __next__() vão apenas levantar a exceção StopIteration novamente. Iteradores precisam ter um método __iter__() que retorne o objeto iterador em si, de forma que todo iterador também é iterável e pode ser usado na maioria dos lugares em que um iterável é requerido. Uma notável exceção é código que tenta realizar passagens em múltiplas iterações. Um objeto contêiner (como uma list) produz um novo iterador a cada vez que você passá-lo para a função iter() ou utilizá-lo em um laço for. Tentar isso com o mesmo iterador apenas iria retornar o mesmo objeto iterador esgotado já utilizado na iteração anterior, como se fosse um contêiner vazio.

Para entender, vamos criar um iterador passando nossa variável numeros para a função iter() atribuindo à uma nova variável num_iter:

num_iter = iter(numeros)

Vamos verificar o tipo da variável gerada:

type(num_iter)
tuple_iterator

Como o próprio tipo mostra, é um iterador. Sendo um iterador, seguindo a documentação, deve haver um método __iter__() e também um __next__(). Vamos conferir:

dir(num_iter)
['__class__',
 '__delattr__',
 '__dir__',
 '__doc__',
 '__eq__',
 '__format__',
 '__ge__',
 '__getattribute__',
 '__gt__',
 '__hash__',
 '__init__',
 '__init_subclass__',
 '__iter__',
 '__le__',
 '__length_hint__',
 '__lt__',
 '__ne__',
 '__new__',
 '__next__',
 '__reduce__',
 '__reduce_ex__',
 '__repr__',
 '__setattr__',
 '__setstate__',
 '__sizeof__',
 '__str__',
 '__subclasshook__']
set(('__iter__', '__next__')) & set(dir(num_iter))
{'__iter__', '__next__'}

Agora vamos entender o que a documentação quer dizer com sucessivas chamadas de next():

next(num_iter)
1

Observe que apenas o primeiro número foi exibido. Vamos fazer mais três chamadas:

next(num_iter)
2
next(num_iter)
3
next(num_iter)
---------------------------------------------------------------------------
StopIteration                             Traceback (most recent call last)
<ipython-input-16-1d816cd6b265> in <module>
----> 1 next(num_iter)

StopIteration:

Conforme descrito, ao esgotar o iterador, uma exceção StopIteration é gerada indicando que todo o iterador foi consumido.

Apenas para esclarecer uma pergunta que pode estar passando pela sua cabeça, não necessariamente um iterável é um iterador. Exemplo:

next(numeros)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-17-2cf59712f126> in <module>
----> 1 next(numeros)

TypeError: 'tuple' object is not an iterator

Veja que numeros em si não é um iterador embora seja um iterável.

Vamos agora conferir que o objeto map gerado na seção anterior também é um iterável. Lembrando:

map(lambda x : x**2, numeros)
<map at 0x7fe4d0507730>

Vamos associar à uma variável para facilitar o manejo do objeto:

num_map = map(lambda x : x**2, numeros)

Vamos ver o tipo do objeto e fazer sucessivas chamadas de next:

type(num_map)
map
next(num_map)
1
next(num_map)
4
next(num_map)
9
next(num_map)
---------------------------------------------------------------------------
StopIteration                             Traceback (most recent call last)
<ipython-input-24-bc498b643d23> in <module>
----> 1 next(num_map)

StopIteration:

Veja que é o mesmo comportamento visto com o objeto criado via iter().

Como a documentação diz que um iterador também é um iterável, podemos passar o objeto map como argumento de nossa função:

eleva_ao_quadrado(map(lambda x : x**2, numeros))
[1, 16, 81]

Como a função anônima já elevava ao quadrado, o loop for dentro da função eleva_ao_quadrado está elevando ao quadrado novamente. Daí os valores obtidos.

O loop for lida automaticamente com o StopIteration, por isso não vemos a exceção quando o iterador é consumido.

Quando o iterador é passado para dentro de um tipo container, é consumido por completo, também sem levantar a exceção:

list(map(lambda x : x**2, numeros))
[1, 4, 9]
tuple(map(lambda x : x**2, numeros))
(1, 4, 9)

Caso o iterador tenha começado a ser consumido e depois tenha sido passado para um container, apenas o resto do iterador fará parte. Por exemplo, vamos recriar nosso iterador map e chamar uma vez next:

num_map = map(lambda x : x**2, numeros)
next(num_map)
1

Agora, vamos passar o iterador para uma lista:

list(num_map)
[4, 9]

Observe que apenas os dois últimos itens aparecem na lista. Afinal, o iterador não tem registro dos itens passados. Isso é o significado da frase “um objeto que representa um fluxo de dados” do glossário. O iterador já havia retornado o item 1, os próximos no fluxo eram 4 e 9, sendo estes passados para a lista quando esta consumiu por completo o iterador.

Caso queira um contexto histórico sobre iteradores na linguagem, recomendo a leitura da PEP 234.

Conclusão

Como deve ter percebido, iteradores e iteráveis aparecem frequentemente na linguagem e talvez você nem tenha percebido que os estava usando em divervos momentos. E a compreensão desses conceitos é crucial para compreender estruturas mais complexas da linguagem como, por exemplo, geradores, que será tópico em breve de um artigo aqui no site.

Gostou desse artigo? Ele faz parte do Python Drops, um conjunto de posts mais curtos voltados para fundamentos falando sobre alguns aspectos da linguagem Python e de programação em geral. Você pode ler mais desses artigos buscando a tag “drops” aqui no site. Até a próxima!

Compartilhe:

2 comentários em “Iteradores e iteráveis em Python”

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

Rolar para cima