Iteradores de Python
Aprende cómo funcionan los iteradores de Python, cómo crear clases iteradoras personalizadas y cuándo usarlos en lugar de listas.
Un iterador es una de las abstracciones más fundamentales de Python. Cada vez que escribes un bucle for, llamas a zip() o usas una comprensión de lista, Python utiliza silenciosamente el protocolo de iterador por debajo. Este capítulo explica qué son los iteradores, cómo crear los tuyos propios, cómo usar el amplio conjunto de iteradores integrados y cuándo los iteradores son la herramienta adecuada para el trabajo.
¿Qué es un iterador?
Python distingue entre dos conceptos relacionados:
- Un iterable es cualquier objeto sobre el que puedes iterar — una
list,tuple,str,dict,seto cualquier objeto cuya clase defina__iter__. Puede producir un iterador, pero no rastrea la posición por sí mismo. - Un iterador es un objeto que rastrea el estado del recorrido. Implementa dos métodos que juntos forman el protocolo de iterador:
__iter__()— devuelve el objeto iterador en sí. Esto permite que los iteradores funcionen dentro de buclesfory otros contextos de iteración.__next__()— devuelve el siguiente valor cada vez que se llama. Cuando no quedan valores, lanzaStopIteration.
La diferencia clave: puedes recorrer una lista tantas veces como quieras porque cada bucle for solicita un iterador nuevo. Un iterador es unidireccional y de un solo uso — una vez agotado, llamar a next() siempre lanza StopIteration.
graph LR
A[Iterator Object] --> B[__iter__]
B --> C[Returns self]
A --> D[__next__]
D --> E[Next Value]
D --> F{No values left?}
F -->|Yes| G[Raises StopIteration]
F -->|No| ECómo usa los iteradores un bucle for
El bucle for no es más que azúcar sintáctico para el protocolo de iterador. Internamente, Python traduce:
for item in some_iterable:
print(item)en algo así:
_it = iter(some_iterable) # call __iter__()
while True:
try:
item = next(_it) # call __next__()
except StopIteration:
break
print(item)Comprender esta traducción aclara por qué cualquier objeto que implemente __iter__ y __next__ funciona sin problemas en un bucle for, con zip(), enumerate() y cualquier otro contexto que espere un iterable.
Construyendo un iterador personalizado
Para crear un iterador personalizado, define una clase que implemente tanto __iter__ como __next__. A continuación se muestra un iterador Countdown que cuenta hacia atrás desde un número dado hasta 1:
class Countdown:
def __init__(self, start):
self.current = start
def __iter__(self):
return self # the iterator is its own iterable
def __next__(self):
if self.current <= 0:
raise StopIteration
value = self.current
self.current -= 1
return value
for n in Countdown(5):
print(n)
# Output:
# 5
# 4
# 3
# 2
# 1Observa que __iter__ devuelve self. Esto es lo que permite colocar el mismo objeto directamente en un bucle for — el bucle llama a iter() sobre él, lo que llama a __iter__(), que devuelve el propio iterador.
Añadiendo un parámetro de paso
Puedes añadir cualquier lógica que desees dentro de __next__. A continuación se muestra un iterador StepRange que imita a range() pero acepta un valor de paso:
class StepRange:
def __init__(self, start, stop, step=1):
self.current = start
self.stop = stop
self.step = step
def __iter__(self):
return self
def __next__(self):
if self.current >= self.stop:
raise StopIteration
value = self.current
self.current += self.step
return value
print(list(StepRange(0, 10, 3)))
# Output: [0, 3, 6, 9]Llamar a list() sobre cualquier iterador lo agota y recopila todos los valores en una lista — un patrón útil cuando necesitas todos los resultados a la vez.
Las funciones integradas iter() y next()
Las funciones integradas iter() y next() son la forma estándar de trabajar directamente con el protocolo de iterador.
iter(obj)— llama aobj.__iter__()y devuelve el iterador resultante.next(it)— llama ait.__next__()y devuelve el siguiente valor.next(it, default)— devuelvedefaulten lugar de lanzarStopIterationcuando el iterador está agotado. Es la forma más segura de obtener el siguiente elemento sin un bloque try/except.
words = ["hello", "world"]
it = iter(words)
print(next(it)) # hello
print(next(it)) # world
print(next(it, "done")) # done (exhausted; returns default)La forma de dos argumentos de next() es especialmente útil en escenarios de transmisión o análisis donde deseas manejar el fin de la entrada de forma elegante.
Los iteradores son de un solo uso
Esta es la trampa más común con los iteradores: una vez agotado, un iterador no puede rebobinarse.
it = iter([1, 2, 3])
for x in it:
print(x) # prints 1, 2, 3
for x in it:
print(x) # prints nothing — iterator is exhaustedSi necesitas iterar varias veces, conserva el iterable original (por ejemplo, la lista) y llama a iter() de nuevo, o usa una comprensión de lista para materializar todos los valores primero.
Funciones integradas que devuelven iteradores
La biblioteca estándar de Python está construida sobre iteradores. Todas estas funciones devuelven iteradores en lugar de listas, por lo que son eficientes en memoria incluso con secuencias muy grandes:
range()
range(start, stop, step) devuelve un iterador de enteros. No almacena los enteros en memoria — los calcula bajo demanda.
for i in range(1, 6):
print(i)
# Output: 1 2 3 4 5zip()
zip() toma múltiples iterables y devuelve un iterador de tuplas, emparejando los elementos posición a posición. La iteración se detiene en la entrada más corta.
names = ["Alice", "Bob", "Carol"]
scores = [95, 88, 72]
for name, score in zip(names, scores):
print(f"{name}: {score}")
# Output:
# Alice: 95
# Bob: 88
# Carol: 72enumerate()
enumerate() envuelve cualquier iterable y devuelve pares (índice, valor). Úsalo para evitar mantener una variable contadora manual.
fruits = ["apple", "banana", "cherry"]
for i, fruit in enumerate(fruits, start=1):
print(f"{i}. {fruit}")
# Output:
# 1. apple
# 2. banana
# 3. cherrymap() y filter()
Ambas funciones devuelven iteradores (en Python 3). map(fn, iterable) aplica una función a cada elemento; filter(fn, iterable) conserva solo los elementos para los que la función devuelve True.
numbers = [1, 2, 3, 4, 5]
doubled = list(map(lambda x: x * 2, numbers))
print(doubled) # [2, 4, 6, 8, 10]
evens = list(filter(lambda x: x % 2 == 0, numbers))
print(evens) # [2, 4]Verificar si un objeto es un iterador
Usa isinstance() con las clases base abstractas del módulo collections.abc para comprobar si un objeto es iterable o un iterador:
from collections.abc import Iterable, Iterator
my_list = [1, 2, 3]
my_iter = iter(my_list)
print(isinstance(my_list, Iterable)) # True — list is iterable
print(isinstance(my_list, Iterator)) # False — list is NOT an iterator
print(isinstance(my_iter, Iterator)) # True — list_iterator is an iterator
print(isinstance(my_iter, Iterable)) # True — all iterators are also iterablesTodo iterador es también un iterable (porque __iter__ devuelve self), pero no todo iterable es un iterador.
Cuándo usar iteradores frente a listas
| Situación | Usar |
|---|---|
Necesitas acceso aleatorio (items[5]) | list |
| Necesitas iterar una vez, la memoria importa | iterador / generador |
| Secuencias infinitas o muy grandes | iterador / generador |
| Necesitas iterar varias veces | list (conserva el original) |
| Cadena de transformaciones | iteradores encadenados (map, filter, itertools) |
Para conjuntos de datos grandes — leer millones de filas de un archivo, procesar datos en flujo — un iterador evita cargar todo en memoria a la vez. Para colecciones pequeñas y finitas donde accedes a los elementos repetidamente, una lista es más sencilla.
Iteradores frente a generadores
Un generador es una forma abreviada conveniente de escribir un iterador. En lugar de una clase con __iter__ y __next__, escribes una función que usa yield. Python la convierte automáticamente en un iterador.
# Iterator class
class Countdown:
def __init__(self, start):
self.current = start
def __iter__(self):
return self
def __next__(self):
if self.current <= 0:
raise StopIteration
value = self.current
self.current -= 1
return value
# Equivalent generator function
def countdown(start):
while start > 0:
yield start
start -= 1
print(list(countdown(5))) # [5, 4, 3, 2, 1]Usa un iterador basado en clases cuando necesites métodos adicionales o estado mutable más allá de lo que ofrece un generador simple. Usa un generador para la mayoría de los demás casos — es más conciso e igual de potente.
Consulta el capítulo Generadores de Python para un tratamiento completo de yield, expresiones generadoras y send().