35. Cómo funciona la iteración: iterables e iteradores
A lo largo de este libro, has estado usando bucles (loop) for para iterar sobre listas, cadenas, diccionarios y otras colecciones. Has escrito código como for item in my_list: incontables veces. Pero, ¿qué ocurre realmente entre bastidores cuando Python ejecuta un bucle for? ¿Cómo sabe Python cómo recorrer distintos tipos de colecciones?
En este capítulo, exploraremos el protocolo de iteración de Python: el mecanismo que hace que funcionen los bucles for. Aprenderás sobre iterables (objetos sobre los que puedes iterar) e iteradores (objetos que hacen el avance real a través de los valores). Comprender esta distinción profundizará tu conocimiento de cómo funciona Python y te preparará para trabajar con generadores en el Capítulo 36.
35.1) Qué significa que un objeto sea iterable
35.1.1) El concepto de iterabilidad
Un iterable (iterable) es cualquier objeto de Python sobre el que se puede iterar con un bucle for. Cuando decimos “iterar”, nos referimos a que Python puede obtener elementos del objeto de uno en uno, en secuencia.
Ya has trabajado con muchos iterables:
# Las listas son iterables
numbers = [1, 2, 3, 4, 5]
for num in numbers:
print(num) # Output: 1, 2, 3, 4, 5 (on separate lines)
# Las cadenas son iterables
text = "Python"
for char in text:
print(char) # Output: P, y, t, h, o, n (on separate lines)
# Los diccionarios son iterables (sobre las claves por defecto)
student = {"name": "Alice", "age": 20, "grade": "A"}
for key in student:
print(key) # Output: name, age, grade (on separate lines)Todos estos objetos—listas, cadenas, diccionarios, tuplas, conjuntos, rangos y archivos—son iterables porque admiten el protocolo de iteración de Python (un conjunto de reglas que permite a Python iterar sobre ellos).
35.1.2) Qué hace que un objeto sea iterable
Para que un objeto sea iterable, debe implementar un método especial llamado __iter__(). Este método devuelve un objeto iterador (iterator). No te preocupes por los detalles todavía; exploraremos los iteradores en la siguiente sección.
Puedes comprobar si un objeto es iterable intentando obtener un iterador a partir de él usando la función integrada iter():
# Probar si los objetos son iterables
numbers = [1, 2, 3]
iterator = iter(numbers) # Funciona: las listas son iterables
print(type(iterator)) # Output: <class 'list_iterator'>
text = "Hello"
iterator = iter(text) # Funciona: las cadenas son iterables
print(type(iterator)) # Output: <class 'str_iterator'>
# Probar con un objeto no iterable
value = 42
try:
iterator = iter(value) # Falla: los enteros no son iterables
except TypeError as e:
print(f"Error: {e}") # Output: Error: 'int' object is not iterableCuando llamas a iter() sobre un objeto iterable, Python llama al método __iter__() del objeto y devuelve un iterador. Si el objeto no tiene este método, obtienes un TypeError.
35.1.3) Iterables vs secuencias
Es importante entender que no todos los iterables son secuencias. Una secuencia es un tipo específico de iterable que admite indexación y tiene un orden definido.
# Las secuencias admiten indexación
my_list = [10, 20, 30]
print(my_list[0]) # Output: 10
my_string = "Python"
print(my_string[2]) # Output: t
# Los conjuntos son iterables pero NO son secuencias (sin indexación, sin orden garantizado)
my_set = {1, 2, 3}
for item in my_set:
print(item) # Funciona: los conjuntos son iterables
# Pero la indexación no funciona
try:
print(my_set[0]) # Falla: los conjuntos no admiten indexación
except TypeError as e:
print(f"Error: {e}") # Output: Error: 'set' object is not subscriptableDistinción clave: Todas las secuencias (listas, tuplas, cadenas, rangos) son iterables, pero no todos los iterables son secuencias. Los conjuntos y los diccionarios son iterables pero no secuencias porque no admiten indexación.
35.1.4) Por qué importa la iterabilidad
Comprender la iterabilidad te ayuda a:
- Saber sobre qué puedes iterar: Cualquier iterable funciona con bucles
for - Entender los mensajes de error: “object is not iterable” significa que no puedes usarlo en un bucle
for - Usar comprensiones: Las comprensiones de listas, conjuntos y diccionarios funcionan con cualquier iterable
- Trabajar con funciones integradas: Muchas funciones integradas como
sum(),max(),min()ysorted()aceptan cualquier iterable
# Todo esto funciona porque aceptan iterables
numbers = [1, 2, 3, 4, 5]
print(sum(numbers)) # Output: 15
text = "Python"
print(max(text)) # Output: y (highest alphabetically)
# Incluso funciona con conjuntos
unique_values = {10, 5, 20, 15}
print(sorted(unique_values)) # Output: [5, 10, 15, 20]35.2) Iteradores cotidianos en Python (archivos, rangos, diccionarios y más)
35.2.1) Qué es un iterador
Un iterador (iterator) es un objeto que representa un flujo de datos. Devuelve un valor cada vez cuando pides el siguiente elemento. Una vez que un iterador ha devuelto todos sus valores, queda agotado y no se puede reutilizar.
Piensa en un iterador como un marcador en un libro:
- Recuerda en qué parte estás en la secuencia
- Puedes pedir el siguiente elemento
- Una vez que llegas al final, no puedes volver atrás sin crear un nuevo iterador
La diferencia clave entre un iterable y un iterador:
- Un iterable es algo que puedes iterar (como una lista)
- Un iterador es el objeto que hace la iteración (el mecanismo que avanza por la lista)
# Una lista es un iterable
numbers = [1, 2, 3]
# Obtener un iterador a partir del iterable
iterator = iter(numbers)
# El iterador es un objeto separado
print(type(numbers)) # Output: <class 'list'>
print(type(iterator)) # Output: <class 'list_iterator'>35.2.2) Iteradores en bucles for
Cuando escribes un bucle for, Python crea automáticamente un iterador entre bastidores:
numbers = [10, 20, 30]
# Lo que escribes:
for num in numbers:
print(num)
# Lo que Python hace internamente (conceptualmente):
# 1. Llama a iter(numbers) para obtener un iterador
# 2. Llama repetidamente a next() sobre el iterador
# 3. Se detiene cuando el iterador lanza StopIterationAsí es como se ve eso de forma explícita:
numbers = [10, 20, 30]
# Iteración manual (lo que for hace automáticamente)
iterator = iter(numbers)
try:
print(next(iterator)) # Output: 10
print(next(iterator)) # Output: 20
print(next(iterator)) # Output: 30
print(next(iterator)) # Would raise StopIteration
except StopIteration:
print("No more items") # Output: No more itemsEl bucle for maneja la excepción StopIteration automáticamente, por eso nunca la ves en código normal.
35.2.3) Objetos archivo como iteradores
Los objetos archivo son excelentes ejemplos de iteradores. Cuando iteras sobre un archivo, lee una línea cada vez:
# Crear un archivo de ejemplo
with open("students.txt", "w") as file:
file.write("Alice\n")
file.write("Bob\n")
file.write("Charlie\n")
# Leer el archivo línea por línea
with open("students.txt", "r") as file:
for line in file:
print(line.strip()) # Output: Alice, Bob, Charlie (on separate lines)Los objetos archivo son tanto iterables como iteradores. Se devuelven a sí mismos cuando llamas a iter() sobre ellos:
with open("students.txt", "r") as file:
iterator = iter(file)
print(file is iterator) # Output: True (same object)
# Leer líneas manualmente
print(next(iterator)) # Output: Alice
print(next(iterator)) # Output: Bob
print(next(iterator)) # Output: CharlieEsto es eficiente en memoria porque Python no carga el archivo completo en memoria; lee una línea cada vez según la vas solicitando.
35.2.4) Objetos range como iteradores
Los objetos range son iterables que generan números bajo demanda:
# Un range es un iterable
numbers = range(1, 4)
print(type(numbers)) # Output: <class 'range'>
# Obtener un iterador del range
iterator = iter(numbers)
print(type(iterator)) # Output: <class 'range_iterator'>
# Usar el iterador
print(next(iterator)) # Output: 1
print(next(iterator)) # Output: 2
print(next(iterator)) # Output: 3Los rangos son eficientes en memoria porque no almacenan todos los números en memoria; calculan cada número cuando se solicita:
# Este rango representa 1 millón de números, pero usa memoria mínima
large_range = range(1000000)
print(type(large_range)) # Output: <class 'range'>
# Obtener un iterador
iterator = iter(large_range)
print(next(iterator)) # Output: 0
print(next(iterator)) # Output: 1
# ... puede continuar para 1 millón de valores35.2.5) Iteradores de diccionarios
Los diccionarios proporcionan distintos iteradores para claves, valores y elementos:
student = {"name": "Alice", "age": 20, "grade": "A"}
# Iterar sobre claves (por defecto)
for key in student:
print(key) # Output: name, age, grade (on separate lines)
# Obtener explícitamente un iterador de claves
keys_iterator = iter(student.keys())
print(next(keys_iterator)) # Output: name
print(next(keys_iterator)) # Output: age
# Iterar sobre valores
values_iterator = iter(student.values())
print(next(values_iterator)) # Output: Alice
print(next(values_iterator)) # Output: 20
# Iterar sobre elementos (pares clave-valor)
items_iterator = iter(student.items())
print(next(items_iterator)) # Output: ('name', 'Alice')
print(next(items_iterator)) # Output: ('age', 20)35.2.6) Los iteradores se agotan
Una propiedad crucial de los iteradores es que solo se pueden usar una vez. Una vez agotados, no se reinician:
numbers = [1, 2, 3]
iterator = iter(numbers)
# Primera pasada por el iterador
print(next(iterator)) # Output: 1
print(next(iterator)) # Output: 2
print(next(iterator)) # Output: 3
# El iterador ahora está agotado
try:
print(next(iterator)) # Raises StopIteration
except StopIteration:
print("Iterator exhausted") # Output: Iterator exhausted
# Para iterar de nuevo, crea un nuevo iterador
iterator = iter(numbers)
print(next(iterator)) # Output: 1 (fresh start)Esto es diferente del propio iterable, que puede iterarse varias veces:
numbers = [1, 2, 3]
# Primera iteración
for num in numbers:
print(num) # Output: 1, 2, 3
# Segunda iteración (funciona bien: crea un nuevo iterador)
for num in numbers:
print(num) # Output: 1, 2, 335.3) Usar iter() y next() para avanzar por iterables
35.3.1) La función iter()
La función iter() toma un iterable y devuelve un iterador. Este es el primer paso del protocolo de iteración:
# Crear iteradores a partir de distintos iterables
numbers = [10, 20, 30]
iterator = iter(numbers)
print(type(iterator)) # Output: <class 'list_iterator'>
text = "Hi"
text_iterator = iter(text)
print(type(text_iterator)) # Output: <class 'str_iterator'>
my_set = {1, 2, 3}
set_iterator = iter(my_set)
print(type(set_iterator)) # Output: <class 'set_iterator'>Cada tipo de iterable devuelve su propio tipo de iterador especializado, pero todos funcionan de la misma manera: llamas a next() para obtener el siguiente valor.
35.3.2) La función next()
La función next() obtiene el siguiente elemento de un iterador. Cuando no hay más elementos, lanza StopIteration:
colors = ["red", "green", "blue"]
iterator = iter(colors)
# Obtener elementos de uno en uno
print(next(iterator)) # Output: red
print(next(iterator)) # Output: green
print(next(iterator)) # Output: blue
# No hay más elementos
try:
print(next(iterator)) # Raises StopIteration
except StopIteration:
print("No more colors") # Output: No more colors35.3.3) Proporcionar un valor por defecto a next()
Puedes proporcionar un valor por defecto a next() como segundo argumento. Cuando el iterador se agota, en lugar de lanzar una excepción StopIteration, next() devolverá el valor por defecto que especificaste:
numbers = [1, 2, 3]
iterator = iter(numbers)
print(next(iterator)) # Output: 1
print(next(iterator)) # Output: 2
print(next(iterator)) # Output: 3
print(next(iterator, "Done")) # Output: Done (default value, no exception)
print(next(iterator, "Done")) # Output: Done (still exhausted)Esto es útil cuando quieres manejar el final de la iteración de forma elegante sin manejo de excepciones:
35.4) Crear iteradores personalizados con iter y next
35.4.1) Por qué crear iteradores personalizados
Los iterables integrados de Python (listas, cadenas, archivos) cubren la mayoría de los casos comunes. Sin embargo, a veces necesitas crear tus propios objetos iterables para un comportamiento especializado:
- Generar secuencias con lógica personalizada
- Iterar sobre estructuras de datos que diseñes
- Crear iteración eficiente en memoria sobre conjuntos de datos grandes
- Implementar evaluación diferida (lazy evaluation) (calcular valores solo cuando se necesitan)
Crear un iterador personalizado requiere implementar dos métodos especiales: __iter__() y __next__().
35.4.2) El protocolo del iterador
Para que un objeto sea un iterador, debe implementar:
__iter__(): Devuelve el propio objeto iterador (normalmenteself)__next__(): Devuelve el siguiente valor de la secuencia, o lanzaStopIterationcuando termina
class SimpleCounter:
"""An iterator that counts from start to end."""
def __init__(self, start, end):
self.current = start
self.end = end
def __iter__(self):
"""Return the iterator object (self)."""
return self
def __next__(self):
"""Return the next value or raise StopIteration."""
if self.current > self.end:
raise StopIteration
value = self.current
self.current += 1
return value
# Usar el iterador personalizado
counter = SimpleCounter(1, 5)
for num in counter:
print(num)
# Output: 1
# Output: 2
# Output: 3
# Output: 4
# Output: 5Desglosemos lo que ocurre:
- El bucle
forllama aiter(counter), lo que llama acounter.__iter__()y devuelvecounteren sí - El bucle llama repetidamente a
next(counter), lo que llama acounter.__next__() - Cada llamada a
__next__()devuelve el siguiente número e incrementacurrent - Cuando
current > end,__next__()lanzaStopIteration, y el bucle se detiene
35.4.3) Uso manual de iteradores personalizados
También puedes usar iteradores personalizados manualmente con iter() y next():
counter = SimpleCounter(10, 13)
# Obtener el iterador (se devuelve a sí mismo)
iterator = iter(counter)
print(iterator is counter) # Output: True
# Obtener valores manualmente
print(next(iterator)) # Output: 10
print(next(iterator)) # Output: 11
print(next(iterator)) # Output: 12
print(next(iterator)) # Output: 13
# Ahora está agotado
try:
print(next(iterator))
except StopIteration:
print("Counter exhausted") # Output: Counter exhausted35.4.4) Los iteradores se agotan (revisitado)
Recuerda que los iteradores solo se pueden usar una vez:
counter = SimpleCounter(1, 3)
# Primera iteración
for num in counter:
print(num) # Output: 1, 2, 3
# Segunda iteración (no funciona: el iterador está agotado)
for num in counter:
print(num) # Nothing printed - iterator is already exhaustedPara iterar de nuevo, necesitas crear una nueva instancia:
# Crear un nuevo contador para cada iteración
for num in SimpleCounter(1, 3):
print(num) # Output: 1, 2, 3
for num in SimpleCounter(1, 3):
print(num) # Output: 1, 2, 3 (new iterator)35.4.5) Crear una clase iterable (no solo un iterador)
A menudo, quieres una clase que sea iterable pero que cree un iterador nuevo cada vez. Para hacerlo, separa el iterable del iterador:
class CounterIterable:
"""An iterable that creates fresh counter iterators."""
def __init__(self, start, end):
self.start = start
self.end = end
def __iter__(self):
"""Return a new iterator each time."""
return CounterIterator(self.start, self.end)
class CounterIterator:
"""The actual iterator that does the counting."""
def __init__(self, start, end):
self.current = start
self.end = end
def __iter__(self):
return self
def __next__(self):
if self.current > self.end:
raise StopIteration
value = self.current
self.current += 1
return value
# Ahora podemos iterar varias veces
counter = CounterIterable(1, 3)
# Primera iteración
for num in counter:
print(num) # Output: 1, 2, 3
# Segunda iteración (funciona porque __iter__ crea un nuevo iterador)
for num in counter:
print(num) # Output: 1, 2, 3Este patrón separa responsabilidades:
CounterIterablees el iterable: sabe cómo crear iteradoresCounterIteratores el iterador: sabe cómo avanzar por los valores
35.4.6) Ejemplo práctico: iterar sobre una estructura de datos personalizada
Creemos un iterador para una estructura de datos personalizada: una lista de reproducción simple:
class Playlist:
"""A music playlist that can be iterated over."""
def __init__(self):
self.songs = []
def add_song(self, title, artist):
"""Add a song to the playlist."""
self.songs.append({"title": title, "artist": artist})
def __iter__(self):
"""Return an iterator for the playlist."""
return PlaylistIterator(self.songs)
class PlaylistIterator:
"""Iterator for stepping through songs in a playlist."""
def __init__(self, songs):
self.songs = songs
self.index = 0
def __iter__(self):
return self
def __next__(self):
if self.index >= len(self.songs):
raise StopIteration
song = self.songs[self.index]
self.index += 1
return song
# Usar la lista de reproducción
playlist = Playlist()
playlist.add_song("Imagine", "John Lennon")
playlist.add_song("Bohemian Rhapsody", "Queen")
playlist.add_song("Hotel California", "Eagles")
# Iterar sobre canciones
print("Now playing:")
for song in playlist:
print(f" {song['title']} by {song['artist']}")
# Output: Now playing:
# Output: Imagine by John Lennon
# Output: Bohemian Rhapsody by Queen
# Output: Hotel California by Eagles
# Se puede iterar de nuevo (crea un nuevo iterador)
print("\nReplay:")
for song in playlist:
print(f" {song['title']}")
# Output: Replay:
# Output: Imagine
# Output: Bohemian Rhapsody
# Output: Hotel California35.4.7) Cuándo usar iteradores personalizados
Crea iteradores personalizados cuando:
- Necesitas evaluación diferida: Generar valores bajo demanda en lugar de almacenarlos todos
- Tienes una estructura de datos personalizada: Hacerla iterable para que funcione con bucles
for - Necesitas lógica especial de iteración: Omitir elementos, transformar valores o implementar un avance complejo
- La eficiencia de memoria importa: Generar secuencias grandes sin almacenarlas
Sin embargo, en el Capítulo 36, aprenderás sobre generadores, que ofrecen una forma mucho más simple de crear iteradores usando la palabra clave yield. Los generadores suelen preferirse frente a implementar manualmente __iter__() y __next__() porque son más concisos y más fáciles de entender.
Comprender cómo crear iteradores personalizados te da una visión de cómo funciona el protocolo de iteración de Python, incluso si a menudo usarás generadores en su lugar. Los conceptos que has aprendido aquí—__iter__(), __next__() y StopIteration—son fundamentales para entender los generadores y otras técnicas avanzadas de iteración en el siguiente capítulo.