Saltar al contenido principal

Tutorial de Iteradores y Generadores de Python

Explore la diferencia entre Iteradores y Generadores de Python y aprenda cuáles son los mejores para usar en diversas situaciones.
3 may 2024  · 10 min de lectura

Los iteradores son objetos sobre los que se puede iterar. Se trata de una característica común del lenguaje de programación Python, perfectamente escondida para bucles y comprensión de listas. Cualquier objeto que pueda derivar un iterador se conoce como iterable. 

La construcción de un iterador requiere mucho trabajo. Por ejemplo, la implementación de cada objeto iterador debe constar de un método __iter__() y __next__() . Además del prerrequisito anterior, la implementación también debe tener una forma de rastrear el estado interno del objeto y lanzar una excepción StopIteration una vez que no se puedan devolver más valores. Estas reglas se conocen como protocolo iterador

Implementar tu propio iterador es un proceso largo y sólo a veces necesario. Una alternativa más sencilla es utilizar un objeto generador. Los generadores son un tipo especial de función que utiliza la palabra clave yield para devolver un iterador sobre el que se puede iterar, un valor cada vez. 

La capacidad de discernir los escenarios apropiados para implementar un iterador o utilizar un generador mejorará tus habilidades como programador de Python. En el resto de este tutorial, haremos hincapié en las diferencias entre ambos objetos, lo que le ayudará a decidir cuál es el mejor para cada situación. 

Glosario

Plazo

Definición

Iterable 

Objeto de Python sobre el que se puede hacer un bucle o iterar en un bucle. Algunos ejemplos de iterables son las listas, los conjuntos, las tuplas, los diccionarios, las cadenas, etc. 

Iterador

Un iterador es un objeto sobre el que se puede iterar. Así, los iteradores contienen un número contable de valores. 

Generador

Un tipo especial de función que no devuelve un único valor: devuelve un objeto iterador con una secuencia de valores.

Evaluación perezosa 

Una estrategia de evaluación por la que determinados objetos sólo se producen cuando son necesarios. En consecuencia, ciertos círculos de desarrolladores también se refieren a la evaluación perezosa como "llamada por necesidad".

Protocolo de iteración 

Conjunto de reglas que deben seguirse para definir un iterador en Python. 

next()

Función integrada que se utiliza para devolver el siguiente elemento de un iterador. 

iter()

Función incorporada que se utiliza para convertir un iterable en un iterador. 

yield()

Una palabra clave de python similar a la palabra clave return, excepto que yield devuelve un objeto generador en lugar de un valor. 

Iteradores e iterables de Python

Los iterables son objetos capaces de devolver sus miembros de uno en uno: se puede iterar sobre ellos. Las estructuras de datos incorporadas en Python más populares, como listas, tuplas y conjuntos, se consideran iterables. Otras estructuras de datos, como las cadenas y los diccionarios, también se consideran iterables: una cadena puede producir iteración de sus caracteres, y sobre las claves de un diccionario se puede iterar. Como regla general, considere iterable cualquier objeto sobre el que se pueda iterar en un bucle for. 

Explorar los iterables de Python con ejemplos

Dadas las definiciones, podemos concluir que todos los iteradores son también iterables. Sin embargo, todo iterable no es necesariamente un iterador. Un iterable produce un iterador sólo una vez que se itera sobre él.

Para demostrar esta funcionalidad, instanciaremos una lista, que es un iterable, y produciremos un iterador llamando a la función incorporada iter() sobre la lista. 

list_instance = [1, 2, 3, 4]
print(iter(list_instance))

"""
<list_iterator object at 0x7fd946309e90>
"""

Aunque la lista por sí misma no es un iterador, llamar a la función iter() la convierte en un iterador y devuelve el objeto iterador.

Para demostrar que no todos los iterables son iteradores, instanciaremos el mismo objeto lista e intentaremos llamar a la función next(), que se utiliza para devolver el siguiente elemento de un iterador.  

list_instance = [1, 2, 3, 4]
print(next(list_instance))
"""
--------------------------------------------------------------------
TypeError                         Traceback (most recent call last)
<ipython-input-2-0cb076ed2d65> in <module>()
    3 print(iter(list_instance))
    4
----> 5 print(next(list_instance))
TypeError: 'list' object is not an iterator
"""

En el código anterior, se puede ver que el intento de llamar a la función next() en la lista planteó un TypeError - aprender más acerca de Excepción y Manejo de Errores en Python. Este comportamiento se produce por el simple hecho de que un objeto lista es un iterable y no un iterador. 

Explorar los iteradores de Python con ejemplos

Por lo tanto, si el objetivo es iterar sobre una lista, primero se debe producir un objeto iterador. Sólo entonces podremos gestionar la iteración a través de los valores de la lista.

# instantiate a list object
list_instance = [1, 2, 3, 4]

# convert the list to an iterator
iterator = iter(list_instance)

# return items one at a time
print(next(iterator))
print(next(iterator))
print(next(iterator))
print(next(iterator))
"""
1
2
3
4
"""

Python produce automáticamente un objeto iterador cada vez que se intenta recorrer un objeto iterable. 

# instantiate a list object
list_instance = [1, 2, 3, 4]

# loop through the list
for iterator in list_instance:
  print(iterator)
"""
1
2
3
4
"""

Cuando se captura la excepción StopIteration, el bucle finaliza.

Los valores obtenidos de un iterador sólo pueden recuperarse de izquierda a derecha. Python no tiene una función previous() para permitir a los desarrolladores moverse hacia atrás a través de un iterador. 

La pereza de los iteradores

Es posible definir múltiples iteradores basados en el mismo objeto iterable. Cada iterador mantendrá su propio estado de progreso. Así, definiendo múltiples instancias iteradoras de un objeto iterable, es posible iterar hasta el final de una instancia mientras la otra instancia permanece al principio.

list_instance = [1, 2, 3, 4]
iterator_a = iter(list_instance)
iterator_b = iter(list_instance)
print(f"A: {next(iterator_a)}")
print(f"A: {next(iterator_a)}")
print(f"A: {next(iterator_a)}")
print(f"A: {next(iterator_a)}")
print(f"B: {next(iterator_b)}")
"""
A: 1
A: 2
A: 3
A: 4
B: 1
"""

Observe que iterator_b imprime el primer elemento de la serie.

Así, podemos decir que los iteradores tienen una naturaleza perezosa: cuando se crea un iterador, los elementos no se ceden hasta que se solicitan. En otras palabras, los elementos de nuestra instancia de lista sólo se devolverían una vez que los pidiéramos explícitamente con next(iter(list_instance))

Sin embargo, todos los valores de un iterador pueden extraerse a la vez llamando a un contenedor de estructura de datos iterable incorporado (es decir, list(), set(), tuple()) sobre el objeto iterador para forzar al iterador a generar todos sus elementos a la vez.

# instantiate iterable
list_instance = [1, 2, 3, 4]

# produce an iterator from an iterable
iterator = iter(list_instance)
print(list(iterator))
"""
[1, 2, 3, 4]
"""

No es recomendable realizar esta acción, especialmente cuando los elementos que devuelve el iterador son grandes, ya que tardará mucho tiempo en procesarse.

Cuando un archivo de datos de gran tamaño satura la memoria de tu máquina, o tienes una función que requiere que su estado interno se mantenga en cada llamada pero crear un iterador no tiene sentido dadas las circunstancias, una alternativa mejor es utilizar un objeto generador.

Generadores Python

La alternativa más conveniente para implementar un iterador es utilizar un generador. Aunque los generadores pueden parecer funciones ordinarias de Python, son diferentes. Para empezar, un objeto generador no devuelve elementos. En su lugar, utiliza la palabra clave yield para generar elementos sobre la marcha. Por lo tanto, podemos decir que un generador es un tipo especial de función que aprovecha la evaluación perezosa.

Los generadores no almacenan su contenido en memoria, como cabría esperar de un iterable típico. Por ejemplo, si el objetivo fuera encontrar todos los factores de un número entero positivo, normalmente implementaríamos una función tradicional (aprende más sobre Funciones Python en este tutorial) como la siguiente:  

def factors(n):
  factor_list = []
  for val in range(1, n+1):
      if n % val == 0:
          factor_list.append(val)
  return factor_list

print(factors(20))
"""
[1, 2, 4, 5, 10, 20]
"""

El código anterior devuelve la lista completa de factores. Sin embargo, observe la diferencia cuando se utiliza un generador en lugar de una función tradicional de Python:

def factors(n):
  for val in range(1, n+1):
      if n % val == 0:
          yield val
print(factors(20))

"""
<generator object factors at 0x7fd938271350>
"""

Dado que hemos utilizado la palabra clave yield en lugar de return, la función no se abandona tras la ejecución. En esencia, le dijimos a Python que creara un objeto generador en lugar de una función tradicional, lo que permite seguir el estado del objeto generador. 

En consecuencia, es posible llamar a la función next() en el iterador perezoso para mostrar los elementos de la serie de uno en uno. 

def factors(n):
  for val in range(1, n+1):
      if n % val == 0:
          yield val
         
factors_of_20 = factors(20)
print(next(factors_of_20))

"""
1
"""

Otra forma de crear un generador es con una comprensión del generador. Las expresiones generadoras adoptan una sintaxis similar a la de una comprensión de lista, salvo que utiliza paréntesis redondeados en lugar de cuadrados.

print((val for val in range(1, 20+1) if n % val == 0))
"""
<generator object <genexpr> at 0x7fd940c31e50>
"""

Exploración de Python yield Palabra clave

La palabra clave yield controla el flujo de una función generadora. En lugar de salir de la función como ocurre cuando se utiliza return, la palabra clave yield devuelve la función pero recuerda el estado de sus variables locales.

El generador devuelto por la llamada a yield puede asignarse a una variable e iterarse con la palabra clave next() - esto ejecutará la función hasta la primera palabra clave yield que encuentre. Cuando se pulsa la palabra clave yield, se suspende la ejecución de la función. Cuando esto ocurre, se guarda el estado de la función. De este modo, podemos reanudar la ejecución de la función a voluntad. 

La función continuará a partir de la llamada a yield. Por ejemplo: 

def yield_multiple_statments():
  yield "This is the first statment"
  yield "This is the second statement"  
  yield "This is the third statement"
  yield "This is the last statement. Don't call next again!"
example = yield_multiple_statments()
print(next(example))
print(next(example))
print(next(example))
print(next(example))
print(next(example))
"""
This is the first statment
This is the second statement
This is the third statement
This is the last statement. Don't call next again or else!
--------------------------------------------------------------------
StopIteration                  Traceback (most recent call last)
<ipython-input-25-4aaf9c871f91> in <module>()
    11 print(next(example))
    12 print(next(example))
---> 13 print(next(example))
StopIteration:
"""

En el código anterior, nuestro generador tiene cuatro llamadas a yield, pero intentamos llamar a next cinco veces, lo que generó una excepción en StopIteration. Este comportamiento se produjo porque nuestro generador no es una serie infinita, por lo que llamarlo más veces de lo esperado agotó el generador.

Recapitulación 

Para recapitular, los iteradores son objetos sobre los que se puede iterar, y los generadores son funciones especiales que aprovechan la evaluación perezosa. Implementar tu propio iterador significa que debes crear un método __iter__() y __next__(), mientras que un generador se puede implementar utilizando la palabra clave yield en una función o comprensión de Python. 

Es posible que prefiera utilizar un iterador personalizado en lugar de un generador cuando necesite un objeto con un comportamiento de mantenimiento de estado complejo o si desea exponer otros métodos además de __next__(), __iter__() y __init__(). Por otro lado, un generador puede ser preferible cuando se trata de grandes conjuntos de datos ya que no almacenan su contenido en memoria o cuando no es necesario implementar un iterador. 

Temas

Los mejores cursos de Python

curso

Introduction to Functions in Python

3 hr
432.6K
Learn the art of writing your own functions in Python, as well as key concepts like scoping and error handling.
Ver detallesRight Arrow
Comienza el curso
Ver másRight Arrow
Relacionado
Python 2 vs 3

blog

Python 2 frente a 3: Todo lo que necesitas saber

En este artículo, trataremos las principales diferencias entre Python 2 y 3, cuál es el mejor y por cuál deberías decantarte para comenzar tu andadura en la ciencia de datos
Javier Canales Luna's photo

Javier Canales Luna

6 min

tutorial

Tutorial de list index() de Python

En este tutorial, aprenderás exclusivamente sobre la función index().
Sejal Jaiswal's photo

Sejal Jaiswal

6 min

tutorial

Tutorial de bucles For en Python

Aprenda a implementar bucles For en Python para iterar una secuencia, o las filas y columnas de un dataframe pandas.
Aditya Sharma's photo

Aditya Sharma

5 min

tutorial

Tutorial de funciones de Python

Un tutorial sobre funciones en Python que cubre cómo escribir funciones, cómo invocarlas y mucho más.
Karlijn Willems's photo

Karlijn Willems

14 min

tutorial

Tutorial de Python sobre conjuntos y teoría de conjuntos

Aprende sobre los conjuntos en Python: qué son, cómo crearlos, cuándo usarlos, funciones incorporadas y su relación con las operaciones de la teoría de conjuntos.
DataCamp Team's photo

DataCamp Team

13 min

tutorial

Tutorial de cadenas en Python

En este tutorial, aprenderás todo sobre las cadenas de Python: trocearlas y encadenarlas, manipularlas y darles formato con la clase Formatter, cadenas f, plantillas y ¡mucho más!
Sejal Jaiswal's photo

Sejal Jaiswal

16 min

Ver másVer más