Você sabe o que é um iterável? E um iterador? Como reconhecer essas estruturas em Python? Responder tais questionamentos é o objetivo desse artigo.
Caso prefira ver em vídeo, clique no player abaixo. O artigo completo se encontra após o vídeo.
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,stretuple) 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
fore 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çãoforfaz 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 numero 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']
Observe 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 exceptionserá levantada. Neste ponto, o objeto iterador se esgotou e quaisquer chamadas subsequentes a seu método__next__()vão apenas levantar a exceçãoStopIterationnovamente. 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 Drops de programação, 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!