Blog

Iteradores en Python: qué hace realmente un bucle for

El post anterior terminó con un zip que salió vacío la segunda vez que lo usaste. Este post explica por qué, y es la idea sobre la que se apoya el resto de la serie.

Una sentencia for no es algo primitivo. Es una forma corta de escribir tres cosas.

El bucle, escrito a mano

names = ['ada', 'grace', 'alan']

it = iter(names)
while True:
    try:
        name = next(it)
    except StopIteration:
        break
    print(name)

Imprime:

ada
grace
alan

Eso es toda la sentencia for. Llama a iter() sobre el objeto. Llama a next() sobre el resultado hasta que lance StopIteration. Atrapa esa excepción y detente.

No pasa nada más. No hay una lista recorrida por índice, ni una longitud que se esté revisando.

Iterable, iterador, y la diferencia 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))

Imprime:

list -> list_iterator
an iterator returns itself:  True
a list hands out a new one:   False

Un iterable es algo de lo que puedes obtener un iterador. Un iterador es lo que recuerda en qué parte vas.

Una lista es un iterable y no guarda ninguna posición: cada for sobre ella empieza de cero, porque cada for pide un iterador nuevo. Esa es la única razón por la que puedes recorrer una lista dos veces.

La posición lo explica todo

names = ['ada', 'grace', 'alan']
it = iter(names)

print(next(it))
for name in it:          # picks up where next() left off
    print('loop:', name)

Imprime:

ada
loop: grace
loop: alan

El for no empezó desde el principio, porque iter(it) devolvió el mismo iterador a medio usar.

Que es exactamente por lo que pasa esto

scores = zip(['ada', 'grace'], [90, 85])

print('first :', list(scores))
print('second:', list(scores))

Imprime:

first : [('ada', 90), ('grace', 85)]
second: []

zip devuelve un iterador, no un iterable capaz de producir iteradores nuevos. El primer list() lo recorrió hasta el final. El segundo lo encontró ya en el final, que es lo que significa «vacío» aquí.

map, filter, reversed, enumerate, un archivo abierto y cualquier generador se comportan igual. Si necesitas los valores dos veces, guárdalos:

pairs = list(zip(['ada', 'grace'], [90, 85]))

print('first :', pairs)
print('second:', pairs)

Imprime:

first : [('ada', 90), ('grace', 85)]
second: [('ada', 90), ('grace', 85)]

Escribir el tuyo

Dos métodos. __iter__ devuelve el iterador, __next__ devuelve el siguiente valor o lanza 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))

Imprime:

3
2
1
[3, 2, 1]
[]

El bucle funciona. La última línea muestra que tiene el mismo defecto que zip: el objeto es su propio iterador, así que carga él mismo la posición, así que sirve para una sola pasada.

Hacerlo reutilizable

Separa los dos trabajos. El contenedor se queda quieto; un objeto aparte hace el recorrido.

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))

Imprime:

[3, 2, 1]
[3, 2, 1]

Ese es el mismo arreglo que tiene una lista, y ahora Countdown2 se comporta como una.

También son veinte líneas para contar hacia atrás desde tres. El próximo post lo deja en tres.

next acepta un valor por defecto

Útil cuando quieres el primer elemento y no quieres cuidar tú mismo el caso vacío.

it = iter(['ada'])
print(next(it, 'nobody'))
print(next(it, 'nobody'))

Imprime:

ada
nobody

Sin ese valor por defecto, esa segunda llamada lanza StopIteration.

Qué recordar

  • for x in cosa significa: iter(cosa), y después next() hasta StopIteration.

  • Un iterable puede producir iteradores. Un iterador guarda la posición y se devuelve a sí mismo desde __iter__.

  • Cualquier cosa que devuelva un iterador —zip, map, filter, un archivo, un generador— sirve para exactamente una pasada.

  • Si escribes __iter__ y __next__ en el mismo objeto, hiciste algo que funciona una vez. Devuelve un iterador nuevo desde __iter__ si lo quieres reutilizable.

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.