Python & AI Tutorials Logo
Programación Python

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:

python
# 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():

python
# 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 iterable

Cuando 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.

python
# 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 subscriptable

Distinció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.

Objetos de Python

Iterables

No iterables

Secuencias

Iterables que no son secuencias

Listas, Tuplas, Cadenas, Rangos

Conjuntos, Diccionarios, Archivos

Números, None, Booleanos

35.1.4) Por qué importa la iterabilidad

Comprender la iterabilidad te ayuda a:

  1. Saber sobre qué puedes iterar: Cualquier iterable funciona con bucles for
  2. Entender los mensajes de error: “object is not iterable” significa que no puedes usarlo en un bucle for
  3. Usar comprensiones: Las comprensiones de listas, conjuntos y diccionarios funcionan con cualquier iterable
  4. Trabajar con funciones integradas: Muchas funciones integradas como sum(), max(), min() y sorted() aceptan cualquier iterable
python
# 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)
python
# 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:

python
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 StopIteration

Así es como se ve eso de forma explícita:

python
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 items

El bucle for maneja la excepción StopIteration automáticamente, por eso nunca la ves en código normal.

No -> StopIteration

for item in iterable:

Python llama a iter(iterable)

Obtiene el objeto iterador

Python llama a next(iterator)

¿Más elementos?

Asignar a item

Ejecutar el cuerpo del bucle

Salir del bucle

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:

python
# 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:

python
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: Charlie

Esto 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:

python
# 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: 3

Los rangos son eficientes en memoria porque no almacenan todos los números en memoria; calculan cada número cuando se solicita:

python
# 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 valores

35.2.5) Iteradores de diccionarios

Los diccionarios proporcionan distintos iteradores para claves, valores y elementos:

python
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:

python
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:

python
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, 3

35.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:

python
# 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:

python
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 colors

35.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:

python
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:

  1. __iter__(): Devuelve el propio objeto iterador (normalmente self)
  2. __next__(): Devuelve el siguiente valor de la secuencia, o lanza StopIteration cuando termina
python
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: 5

Desglosemos lo que ocurre:

  1. El bucle for llama a iter(counter), lo que llama a counter.__iter__() y devuelve counter en sí
  2. El bucle llama repetidamente a next(counter), lo que llama a counter.__next__()
  3. Cada llamada a __next__() devuelve el siguiente número e incrementa current
  4. Cuando current > end, __next__() lanza StopIteration, y el bucle se detiene

35.4.3) Uso manual de iteradores personalizados

También puedes usar iteradores personalizados manualmente con iter() y next():

python
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 exhausted

35.4.4) Los iteradores se agotan (revisitado)

Recuerda que los iteradores solo se pueden usar una vez:

python
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 exhausted

Para iterar de nuevo, necesitas crear una nueva instancia:

python
# 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:

python
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, 3

Este patrón separa responsabilidades:

  • CounterIterable es el iterable: sabe cómo crear iteradores
  • CounterIterator es 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:

python
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 California

35.4.7) Cuándo usar iteradores personalizados

Crea iteradores personalizados cuando:

  1. Necesitas evaluación diferida: Generar valores bajo demanda en lugar de almacenarlos todos
  2. Tienes una estructura de datos personalizada: Hacerla iterable para que funcione con bucles for
  3. Necesitas lógica especial de iteración: Omitir elementos, transformar valores o implementar un avance complejo
  4. 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.

© 2025. Primesoft Co., Ltd.
support@primesoft.ai