26- 馃捇 Python Iterators: Conceptos y Ejemplos




Python Iterators

馃毝‍♂️ Python Iterators: Recorriendo Elementos de Forma Secuencial

En Python, un **iterator** es un objeto que permite recorrer los elementos de una colecci贸n de datos (como listas, tuplas, diccionarios, conjuntos, cadenas, etc.) uno por uno. Los iteradores proporcionan una forma eficiente de acceder a los elementos de una colecci贸n sin necesidad de cargar toda la colecci贸n en la memoria al mismo tiempo. Esto es especialmente 煤til cuando se trabaja con conjuntos de datos grandes.

¿C贸mo Funcionan los Iterators?

Los iteradores en Python se basan en dos m茅todos principales:

  • __iter__(): Este m茅todo se llama en un objeto iterable para obtener el iterador. Si el objeto ya es un iterador, el m茅todo __iter__() devuelve el propio objeto.
  • __next__(): Este m茅todo se llama en el iterador para obtener el siguiente elemento de la secuencia. Cuando no hay m谩s elementos disponibles, el m茅todo __next__() debe levantar una excepci贸n llamada StopIteration.

Ejemplo Sencillo de Iteraci贸n Manual:

Veamos un ejemplo de c贸mo podemos iterar manualmente a trav茅s de una lista utilizando los m茅todos __iter__() y __next__().

      
mi_lista = [1, 2, 3]

# Obtener el iterador de la lista
mi_iterador = iter(mi_lista)

# Acceder a los elementos utilizando next()
print(next(mi_iterador)) # Output: 1
print(next(mi_iterador)) # Output: 2
print(next(mi_iterador)) # Output: 3

# Intentar acceder al siguiente elemento levantar谩 StopIteration
try:
    print(next(mi_iterador))
except StopIteration:
    print("¡Fin de la iteraci贸n!")
      
     

En este ejemplo, la funci贸n incorporada iter() se utiliza para obtener el iterador de la lista mi_lista. Luego, la funci贸n incorporada next() se utiliza para obtener cada elemento del iterador secuencialmente. Una vez que se han recorrido todos los elementos, llamar a next() de nuevo levanta la excepci贸n StopIteration, indicando que no hay m谩s elementos.

Iterators y los Bucles for:

La belleza de los iteradores radica en que los bucles for en Python los utilizan internamente. Cuando escribimos un bucle for para iterar sobre una colecci贸n, Python autom谩ticamente obtiene un iterador para esa colecci贸n y llama a next() en cada iteraci贸n hasta que se levanta la excepci贸n StopIteration, momento en el que el bucle finaliza.

      
mi_lista = [10, 20, 30]
for elemento in mi_lista:
    print(elemento)
# Output:
# 10
# 20
# 30
      
     

En este caso, Python detr谩s de escena hace algo similar a lo que hicimos manualmente en el ejemplo anterior.

En el siguiente tema, exploraremos la diferencia entre iterators e iterables.

Iterator vs Iterable

馃攧 Iterator vs Iterable: No Son Exactamente lo Mismo

En Python, un **iterable** es cualquier objeto que puede devolver sus miembros uno a la vez. Ejemplos comunes de iterables incluyen listas, tuplas, cadenas, diccionarios y conjuntos. T茅cnicamente, un iterable es un objeto que implementa el m茅todo __iter__(), el cual devuelve un objeto iterator.

Un **iterator**, por otro lado, es el objeto actual que realiza la iteraci贸n. Es un objeto que recuerda su estado mientras se recorren los elementos y sabe c贸mo obtener el siguiente valor. Un iterator debe implementar dos m茅todos: __iter__() (que devuelve el propio objeto iterator) y __next__() (que devuelve el siguiente elemento y levanta StopIteration cuando no hay m谩s).

En Resumen:

  • Iterable: Un objeto del cual se puede obtener un iterator. Piensa en 茅l como una colecci贸n que puedes recorrer.
  • Iterator: Un objeto que te permite recorrer un iterable. Recuerda el punto actual en la iteraci贸n.

Analog铆a: Un Libro y un Lector

Una analog铆a 煤til es pensar en un libro y un lector:

  • El libro es el **iterable**. Contiene la informaci贸n (los elementos) que se pueden leer secuencialmente. Puedes volver al principio del libro y leerlo de nuevo.
  • El **lector** es el **iterator**. Mantiene el seguimiento de la p谩gina en la que se encuentra actualmente. El lector avanza p谩gina por p谩gina (llamando a __next__()) hasta que llega al final del libro (cuando se levanta StopIteration). Una vez que el lector termina el libro, necesita un nuevo "lector" (un nuevo iterator) para leerlo de nuevo desde el principio.

Ejemplo para Ilustrar la Diferencia:

      
mi_lista = [1, 2, 3]

# mi_lista es un iterable
print(type(mi_lista)) # Output: <class 'list'>

# Obtener el iterator de mi_lista
mi_iterador = iter(mi_lista)
print(type(mi_iterador)) # Output: <class 'list_iterator'>

# Podemos iterar sobre el iterator
print(next(mi_iterador)) # Output: 1
print(next(mi_iterador)) # Output: 2
print(next(mi_iterador)) # Output: 3

# Si intentamos obtener otro iterator de mi_iterador, obtenemos el mismo objeto
otro_iterador = iter(mi_iterador)
print(otro_iterador is mi_iterador) # Output: True

# Una vez que el primer iterador se agota, el segundo tambi茅n lo est谩
try:
    print(next(otro_iterador))
except StopIteration:
    print("¡El iterador ya se agot贸!")

# Para iterar de nuevo sobre la lista, necesitamos un nuevo iterator
nuevo_iterador = iter(mi_lista)
print(next(nuevo_iterador)) # Output: 1
      
     

Este ejemplo muestra que:

  • Una lista es un iterable.
  • Al llamar a iter() en la lista, obtenemos un objeto iterator.
  • Un iterator puede ser iterado usando next().
  • El m茅todo __iter__() de un iterator devuelve el propio iterator.
  • Una vez que un iterator se ha agotado (ha levantado StopIteration), no se puede reiniciar. Necesitas crear un nuevo iterator a partir del iterable original para recorrer los elementos de nuevo.

En el siguiente tema, veremos c贸mo se recorre un iterator utilizando bucles.

Looping Through an Iterator

➡️ Looping Through an Iterator: La Forma Habitual de Recorrer

Aunque hemos visto c贸mo iterar manualmente a trav茅s de un iterator usando la funci贸n next(), la forma m谩s com煤n y Pyth贸nica de recorrer los elementos de un iterator (y por extensi贸n, un iterable) es utilizando bucles, principalmente el bucle for.

El Bucle for y los Iterators: Una Pareja Perfecta

Como mencionamos anteriormente, el bucle for est谩 dise帽ado para trabajar directamente con iterables. Cuando se itera sobre un iterable con un bucle for, Python autom谩ticamente:

  1. Obtiene un iterator del iterable llamando a su m茅todo __iter__().
  2. En cada iteraci贸n del bucle, llama al m茅todo __next__() del iterator para obtener el siguiente elemento.
  3. Contin煤a hasta que el m茅todo __next__() levanta la excepci贸n StopIteration, momento en el que el bucle finaliza de manera elegante.

Todo este proceso ocurre "detr谩s de las escenas", lo que hace que el c贸digo de iteraci贸n sea muy limpio y f谩cil de leer.

Ejemplo de Iteraci贸n con for:

      
mi_tupla = (100, 200, 300)

for elemento in mi_tupla:
    print(elemento)
# Output:
# 100
# 200
# 300

mi_cadena = "Python"
for caracter in mi_cadena:
    print(caracter)
# Output:
# P
# y
# t
# h
# o
# n
      
     

En ambos casos, mi_tupla y mi_cadena son iterables. El bucle for autom谩ticamente obtiene sus respectivos iterators y los recorre hasta el final.

Otros Bucles que Utilizan Iterators:

Aunque el bucle for es el m谩s com煤n para la iteraci贸n, otras construcciones en Python tambi茅n utilizan el mecanismo de iterators internamente, como:

  • Comprensiones de listas, diccionarios y conjuntos:
    
    cuadrados = [x**2 for x in [1, 2, 3]] # Utiliza un iterator para [1, 2, 3]
          
  • Expresiones generadoras:
    
    generador_cuadrados = (x**2 for x in [1, 2, 3]) # Crea un iterable que genera valores bajo demanda
    for cuadrado in generador_cuadrados:
        print(cuadrado)
          
  • La funci贸n list(), tuple(), set(), etc.: Estas funciones pueden tomar un iterable como argumento y consumen su iterator para crear una nueva lista, tupla o conjunto.
    
    lista_desde_cadena = list("Hola") # Utiliza un iterator de la cadena
    print(lista_desde_cadena) # Output: ['H', 'o', 'l', 'a']
          

En resumen, el bucle for proporciona una forma concisa y legible de iterar sobre cualquier objeto que sea iterable, gracias al uso interno de los iterators.

En el siguiente tema, aprenderemos c贸mo crear nuestros propios iterators.

Create an Iterator

馃洜️ Crear un Iterator Personalizado en Python

Para crear un iterator personalizado, necesitamos definir una clase que cumpla con el protocolo del iterator: implementar los m茅todos __iter__() y __next__().

Implementando __iter__():

El m茅todo __iter__() debe devolver el propio objeto iterator. Esto es necesario para que el objeto sea iterable y pueda ser utilizado en bucles for (que esperan obtener un iterator al llamar a iter() en el objeto).

Implementando __next__():

El m茅todo __next__() debe devolver el siguiente elemento de la secuencia. Cuando no haya m谩s elementos disponibles, este m茅todo debe levantar la excepci贸n StopIteration.

Ejemplo: Un Iterator que Genera N煤meros Pares

Vamos a crear un iterator que genere una secuencia de n煤meros pares hasta un valor m谩ximo especificado.

      
class GeneradorPares:
    def __init__(self, maximo):
        self.maximo = maximo
        self.numero = 0

    def __iter__(self):
        return self

    def __next__(self):
        self.numero += 2
        if self.numero > self.maximo:
            raise StopIteration
        return self.numero

# Crear un iterable (en este caso, la clase misma act煤a como iterable y iterator)
pares_hasta_10 = GeneradorPares(10)

# Iterar sobre el objeto usando un bucle for
for par in pares_hasta_10:
    print(par)
# Output:
# 2
# 4
# 6
# 8
# 10

# Intentar iterar de nuevo no producir谩 nada porque el iterator ya se agot贸
print("Intentando iterar de nuevo:")
for par in pares_hasta_10:
    print(par)
# (No hay output)
      
     

En este ejemplo:

  • La clase GeneradorPares se inicializa con un valor m谩ximo.
  • El m茅todo __iter__() devuelve self, ya que esta clase actuar谩 como su propio iterator.
  • El m茅todo __next__() incrementa el n煤mero actual en 2. Si el n煤mero supera el m谩ximo, levanta StopIteration. En caso contrario, devuelve el n煤mero par actual.

Separando el Iterable del Iterator:

En muchos casos, es m谩s com煤n tener una clase iterable que crea un objeto iterator separado. Esto permite crear m煤ltiples iteradores independientes para el mismo iterable.

      
class MiIterable:
    def __init__(self, data):
        self.data = data

    def __iter__(self):
        return MiIterador(self.data)

class MiIterador:
    def __init__(self, data):
        self.data = data
        self.index = 0

    def __iter__(self):
        return self

    def __next__(self):
        if self.index >= len(self.data):
            raise StopIteration
        value = self.data[self.index]
        self.index += 1
        return value

# Crear un iterable
mi_iterable = MiIterable([10, 20, 30])

# Obtener dos iteradores independientes
iterador1 = iter(mi_iterable)
iterador2 = iter(mi_iterable)

# Iterar sobre el primer iterador
print("Iterador 1:")
print(next(iterador1)) # Output: 10
print(next(iterador1)) # Output: 20

# Iterar sobre el segundo iterador (independiente)
print("\nIterador 2:")
print(next(iterador2)) # Output: 10
print(next(iterador2)) # Output: 20
print(next(iterador2)) # Output: 30

# Continuar con el primer iterador
print("\nIterador 1 (continuaci贸n):")
print(next(iterador1)) # Output: 30

# El segundo iterador ya se agot贸
try:
    print(next(iterador2))
except StopIteration:
    print("¡Iterador 2 agotado!")
      
     

En esta versi贸n, MiIterable es la clase iterable que simplemente almacena los datos y su m茅todo __iter__() crea y devuelve una instancia de MiIterador. La clase MiIterador es la que realmente implementa la l贸gica de la iteraci贸n y mantiene el estado (el 铆ndice actual).

En el siguiente tema, veremos la excepci贸n StopIteration con m谩s detalle.

StopIteration

馃洃 StopIteration: Se帽alando el Fin de la Iteraci贸n

La excepci贸n StopIteration es una excepci贸n incorporada en Python que se utiliza para indicar que un iterator ha agotado todos sus elementos y no hay m谩s valores para devolver.

Cu谩ndo se Levanta StopIteration:

Dentro del m茅todo __next__() de un iterator, una vez que se han recorrido todos los elementos de la secuencia, la siguiente llamada a __next__() debe levantar la excepci贸n StopIteration. Esto es fundamental para que los mecanismos de iteraci贸n de Python (como los bucles for) sepan cu谩ndo detenerse.

El Papel de StopIteration en los Bucles:

Como mencionamos anteriormente, los bucles for funcionan internamente obteniendo un iterator de un iterable y llamando repetidamente a su m茅todo __next__(). El bucle contin煤a ejecut谩ndose para cada valor devuelto por __next__(). Cuando __next__() levanta la excepci贸n StopIteration, el bucle for detecta esta se帽al y finaliza su ejecuci贸n de manera controlada, sin generar un error visible para el usuario.

Ejemplo de StopIteration en un Iterator Personalizado:

Volvamos a nuestro ejemplo del GeneradorPares para ver c贸mo se levanta StopIteration.

      
class GeneradorPares:
    def __init__(self, maximo):
        self.maximo = maximo
        self.numero = 0

    def __iter__(self):
        return self

    def __next__(self):
        self.numero += 2
        if self.numero > self.maximo:
            raise StopIteration
        return self.numero

pares = GeneradorPares(6)

print(next(pares)) # Output: 2
print(next(pares)) # Output: 4
print(next(pares)) # Output: 6

try:
    print(next(pares))
except StopIteration:
    print("¡Se levant贸 StopIteration!") # Output: ¡Se levant贸 StopIteration!
      
     

En este c贸digo, despu茅s de que el iterator pares genera los n煤meros 2, 4 y 6, la siguiente llamada a next(pares) hace que el m茅todo __next__() eval煤e la condici贸n self.numero > self.maximo (que ahora es 8 > 6) como verdadera, lo que provoca que se levante la excepci贸n StopIteration.

Manejo de StopIteration Manualmente:

Aunque los bucles for manejan autom谩ticamente la excepci贸n StopIteration, si est谩s utilizando la funci贸n next() directamente, debes estar preparado para capturar esta excepci贸n utilizando un bloque try...except para evitar que tu programa termine inesperadamente.

      
mi_iterador = iter([1, 2])

print(next(mi_iterador)) # Output: 1
print(next(mi_iterador)) # Output: 2

try:
    print(next(mi_iterador))
except StopIteration:
    print("No hay m谩s elementos.") # Output: No hay m谩s elementos.
      
     

En Resumen:

  • StopIteration es una excepci贸n que los iterators levantan para indicar que han llegado al final de la secuencia.
  • Los bucles for utilizan esta excepci贸n para saber cu谩ndo detener la iteraci贸n.
  • Si trabajas directamente con next(), debes manejar StopIteration para evitar errores.

Con esto, hemos cubierto los aspectos fundamentales de los iterators en Python. ¡Espero que esta lecci贸n haya sido informativa!




Publicar un comentario

0 Comentarios