33 · Iteradores y el protocolo de iterador
Entiende el contrato de dos métodos que impulsa todo bucle for — __iter__ y __next__ — y escribe tu propia clase iteradora personalizada.
Un **iterable** es cualquier objeto con `__iter__()` — una lista, un string, un archivo. Un **iterador** es el cursor con estado que obtienes al llamar `iter(obj)`. Tiene `__next__()` que devuelve un valor a la vez y lanza `StopIteration` cuando la secuencia se agota. Todo bucle `for` es azúcar sintáctico exactamente para este baile de dos pasos.
Sin esto:
Sin entender los iteradores no puedes construir fuentes de datos personalizadas que se conecten al bucle `for` de Python, `zip()`, `enumerate()` o cualquier función que espere un iterable. Frameworks como PyTorch expresan `DataLoader` como un iterador — llamar `iter(loader)` reinicia el cursor para una nueva época.
Python distingue entre dos conceptos relacionados pero distintos:
- Iterable — cualquier objeto que implementa
__iter__(). Una lista, un string, un rango, un archivo son todos iterables. Cuando pides su iterador siempre obtienes uno nuevo. El iterable en sí no rastrea la posición. - Iterador — el cursor con estado producido al llamar
iter(obj). Implementa dos métodos:__iter__()(que simplemente retornaself) y__next__()(que retorna el siguiente valor o lanzaStopIterationpara señalar el final).
La regla clave: puedes llamar iter() en una lista una y otra vez y obtener un iterador fresco cada vez. Un iterador, en cambio, es de un solo uso — una vez agotado permanece agotado. No puedes rebobinar un iterador en bruto; tendrías que llamar iter(iterable_original) de nuevo.
Atajos integrados: iter(obj) es lo mismo que llamar obj.__iter__(), y next(it) es lo mismo que it.__next__().
Python (in browser)
El iterador `it` es un objeto separado de la lista `numbers`. Tras tres llamadas a `next()` el cursor está más allá del final. La lista no cambia — llamar `iter(numbers)` de nuevo produce un iterador completamente nuevo desde el principio.
Python runs entirely in your browser via Pyodide (~6 MB on first Run, cached after).
Python (in browser)
`__iter__` retorna `self` porque la instancia de `Countdown` es tanto el iterable como el iterador — guarda el estado. Este es el patrón estándar para clases iteradoras personalizadas.
Python runs entirely in your browser via Pyodide (~6 MB on first Run, cached after).
Todo bucle `for` se compila exactamente en estas seis líneas. Entender esto hace obvio por qué un iterador solo avanza y por qué `StopIteration` es la señal — no un error — de que la secuencia terminó.
¿Es una lista de Python un iterador?
- **Iterable** = tiene `__iter__()`; **Iterador** = tiene `__iter__()` (retorna self) y `__next__()` (retorna el siguiente valor o lanza `StopIteration`).
- `for x in obj:` llama `iter(obj)`, luego `next(...)` en un bucle hasta `StopIteration` — sin más magia de por medio.
- Las listas son iterables pero no iteradores. Llamar `iter([])` devuelve un `list_iterator` nuevo cada vez — la lista original nunca se muta.
- La mayoría de los iteradores en bruto son de un solo uso — después de `StopIteration` permanecen agotados. Para repetir, llama `iter()` en el iterable original.
El `DataLoader` de PyTorch es un iterador — cada época de entrenamiento llamas `iter(loader)` para obtener un cursor fresco sobre el dataset mezclado. `enumerate(loader)` lo envuelve para dar pares `(batch_idx, batch)`. Las fuentes de datos en streaming (HuggingFace `IterableDataset`, cursores de base de datos, streams de red) se exponen como iteradores porque los datos completos no caben en RAM — debes extraer un chunk a la vez.
Si lo quitas: Sin el protocolo de iteradores, toda pipeline de datos necesitaría materializar el dataset completo en una lista antes de que empiece la primera época. Para datasets de escala ImageNet o en streaming eso es simplemente imposible — el modelo de iteradores es lo que hace viables los bucles de entrenamiento perezosos y eficientes en memoria.