O post anterior terminou com um zip que voltou vazio na segunda vez que você usou. Este post é o porquê, e é a ideia sobre a qual o resto da série é construído.
Uma instrução for não é uma primitiva. É atalho para três coisas.
O laço, escrito na mão
names = ['ada', 'grace', 'alan']
it = iter(names)
while True:
try:
name = next(it)
except StopIteration:
break
print(name)
Ele imprime:
ada
grace
alan
Isso é a instrução for inteira. Chame iter() na coisa. Chame next() no resultado até levantar StopIteration. Capture isso e pare.
Não tem mais nada acontecendo. Não tem lista sendo percorrida por índice, e não tem tamanho sendo conferido.
Iterável, iterador, e a diferença que importa
names = ['ada', 'grace', 'alan']
it = iter(names)
print(type(names).__name__, '->', type(it).__name__)
print('an iterator returns itself: ', iter(it) is it)
print('a list hands out a new one: ', iter(names) is iter(names))
Ele imprime:
list -> list_iterator
an iterator returns itself: True
a list hands out a new one: False
Um iterável é algo de onde você consegue um iterador. Um iterador é a coisa que lembra onde você está.
Uma lista é um iterável, e ela não guarda posição nenhuma — todo for sobre ela começa do zero, porque todo for pede um iterador novo. Esse é o único motivo de você conseguir percorrer uma lista duas vezes.
A posição é a história toda
names = ['ada', 'grace', 'alan']
it = iter(names)
print(next(it))
for name in it: # picks up where next() left off
print('loop:', name)
Ele imprime:
ada
loop: grace
loop: alan
O for não começou do início, porque iter(it) devolveu o mesmo iterador já pela metade.
E é exatamente por isso que isto acontece
scores = zip(['ada', 'grace'], [90, 85])
print('first :', list(scores))
print('second:', list(scores))
Ele imprime:
first : [('ada', 90), ('grace', 85)]
second: []
O zip devolve um iterador, não um iterável capaz de produzir iteradores novos. O primeiro list() levou ele até o fim. O segundo encontrou ele já no fim, e é isso que “vazio” quer dizer aqui.
O map, o filter, o reversed, o enumerate, um arquivo aberto e todo generator se comportam do mesmo jeito. Se você precisa dos valores duas vezes, guarde eles:
pairs = list(zip(['ada', 'grace'], [90, 85]))
print('first :', pairs)
print('second:', pairs)
Ele imprime:
first : [('ada', 90), ('grace', 85)]
second: [('ada', 90), ('grace', 85)]
Escrevendo o seu
Dois métodos. O __iter__ devolve o iterador, o __next__ devolve o próximo valor ou levanta StopIteration.
class Countdown:
def __init__(self, n):
self.n = n
def __iter__(self):
return self
def __next__(self):
if self.n <= 0:
raise StopIteration
self.n -= 1
return self.n + 1
for i in Countdown(3):
print(i)
c = Countdown(3)
print(list(c))
print(list(c))
Ele imprime:
3
2
1
[3, 2, 1]
[]
O laço funciona. A última linha mostra que ele tem o mesmo defeito do zip: o objeto é o próprio iterador, então ele mesmo carrega a posição, então serve para uma passada só.
Deixando reutilizável
Separe os dois trabalhos. O contêiner fica parado; um objeto separado faz a caminhada.
class CountdownIter:
def __init__(self, n):
self.n = n
def __iter__(self):
return self
def __next__(self):
if self.n <= 0:
raise StopIteration
self.n -= 1
return self.n + 1
class Countdown2:
def __init__(self, n):
self.n = n
def __iter__(self):
return CountdownIter(self.n)
c = Countdown2(3)
print(list(c))
print(list(c))
Ele imprime:
[3, 2, 1]
[3, 2, 1]
Esse é o mesmo arranjo que uma lista tem, e agora o Countdown2 se comporta como uma.
Também são vinte linhas para contar de três até um. O próximo post reduz isso para três linhas.
O next aceita um padrão
Útil quando você quer o primeiro item e não quer proteger o caso vazio na mão.
it = iter(['ada'])
print(next(it, 'nobody'))
print(next(it, 'nobody'))
Ele imprime:
ada
nobody
Sem o padrão, aquela segunda chamada levanta StopIteration.
O que lembrar
-
for x in thingquer dizer:iter(thing), depoisnext()atéStopIteration. -
Um iterável produz iteradores. Um iterador guarda a posição e devolve a si mesmo no
__iter__. -
Qualquer coisa que devolve um iterador —
zip,map,filter, um arquivo, um generator — serve para exatamente uma passada. -
Escreva
__iter__e__next__no mesmo objeto e você fez algo que funciona uma vez. Devolva um iterador novo no__iter__se quiser reutilizar.