Blog

Iteradores em Python: o que um for realmente faz

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 thing quer dizer: iter(thing), depois next() 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.

How useful was this post?

Click on a heart to rate it!

Average rating 0 / 5. Vote count: 0

No votes so far! Be the first to rate this post.