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 cosasignifica:iter(cosa), y despuésnext()hastaStopIteration. -
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.