En Python, casi todo lo que puedes recorrer con un bucle for es un iterable. Pero un iterable y un iterador no son lo mismo. Entender la diferencia y saber implementar el protocolo iterador te permite crear objetos que se integran de forma nativa con for, list(), sum() y cualquier función que acepte iterables.
Iterable vs iterador
# Una lista es iterable: tiene __iter__ nums = [1, 2, 3] it = iter(nums) # __iter__ devuelve un iterador # El iterador tiene __next__ print(next(it)) # 1 print(next(it)) # 2 print(next(it)) # 3 # next(it) # StopIteration se agotó # Puedes crear múltiples iteradores del mismo iterable it1 = iter(nums) it2 = iter(nums) next(it1) # 1 next(it2) # 1 independiente de it1
Implementar __iter__ y __next__ en una clase
class ContadorDescendente:
"""Itera de n hasta 1."""
def __init__(self, inicio):
self.inicio = inicio
self.actual = inicio
def __iter__(self):
"""Devuelve el propio objeto como iterador."""
self.actual = self.inicio
return self
def __next__(self):
if self.actual <= 0:
raise StopIteration
valor = self.actual
self.actual -= 1
return valor
cd = ContadorDescendente(5)
for n in cd:
print(n, end=" ") # 5 4 3 2 1
# Se puede reutilizar porque __iter__ reinicia el estado
print(list(cd)) # [5, 4, 3, 2, 1]
Separar iterable de iterador
Cuando el mismo objeto sirve como iterable e iterador (como arriba), tiene el problema de que compartir el objeto entre dos bucles interfieren. La solución es separar el estado de iteración en una clase aparte:
class RangoFibonacci:
"""Iterable de n números de Fibonacci."""
def __init__(self, n):
self.n = n
def __iter__(self):
return IteradorFibonacci(self.n)
class IteradorFibonacci:
def __init__(self, n):
self.n = n
self.a, self.b = 0, 1
self.contador = 0
def __iter__(self):
return self
def __next__(self):
if self.contador >= self.n:
raise StopIteration
valor = self.a
self.a, self.b = self.b, self.a + self.b
self.contador += 1
return valor
fibs = RangoFibonacci(8)
print(list(fibs)) # [0, 1, 1, 2, 3, 5, 8, 13]
print(list(fibs)) # [0, 1, 1, 2, 3, 5, 8, 13] sigue funcionando
Iterador infinito con islice
from itertools import islice
class Naturales:
"""Iterador infinito de números naturales."""
def __init__(self):
self.n = 0
def __iter__(self):
return self
def __next__(self):
self.n += 1
return self.n
nat = Naturales()
print(list(islice(nat, 10))) # [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
print(next(nat)) # 11 continúa donde se quedó
Alternativa con generador
La mayoría de las veces, un generador es más simple que implementar la clase iteradora completa:
def contador_descendente(n):
while n > 0:
yield n
n -= 1
print(list(contador_descendente(5))) # [5, 4, 3, 2, 1]
Implementa el protocolo iterador con clases cuando necesites estado complejo, reinicios controlados o compatibilidad con herencia. Usa generadores para todo lo demás.
