Você sabe o que é um iterável? E um iterador? Como reconhecer essas estruturas em Python? Responder tais questionamentos é o objetivo desse artigo.
Tópicos
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
etuple
) e alguns tipos não sequenciais comodict
, 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 deSequence
.
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 nativaiter()
, 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 chamariter()
ou lidar com os objetos iteradores em si. A instruçãofor
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 embutidanext()
) vão retornar itens sucessivos do fluxo. Quando não houver mais dados disponíveis uma exceçãoStopIteration 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çãoStopIteration
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 umalist
) produz um novo iterador a cada vez que você passá-lo para a funçãoiter()
ou utilizá-lo em um laçofor
. 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!
Olá. Sou Beto.
Muito didático e nível de produção de entendimento muito satisfatório.
Valeu, Beto!