Generadores de Python

Un generador de Python es una función que produce una secuencia de resultados. Funciona manteniendo su estado local, de modo que la función puede reanudarse exactamente cuando...

¿Qué es un generador?

Un [generador] de Python (https://wiki.python.org/moin/Generators) es una función que produce una secuencia de resultados. Funciona manteniendo su estado local, de modo que la función puede reanudarse exactamente donde la dejó cuando se la llame en ocasiones posteriores. Por lo tanto, puede pensar en un generador como algo así como un poderoso iterador.

El estado de la función se mantiene mediante el uso de la palabra clave rendimiento, que tiene la siguiente sintaxis:

1
yield [expression_list]

Esta palabra clave de Python funciona de manera muy similar a return, pero tiene algunas diferencias importantes, que explicaremos a lo largo de este artículo.

Los generadores se introdujeron en PEP 255, junto con la declaración yield. Han estado disponibles desde la versión 2.2 de Python.

¿Cómo funcionan los generadores de Python?

Para entender cómo funcionan los generadores, usemos el siguiente ejemplo simple:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# generator_example_1.py

def numberGenerator(n):
     number = 0
     while number < n:
         yield number
         number += 1

myGenerator = numberGenerator(3)

print(next(myGenerator))
print(next(myGenerator))
print(next(myGenerator))

El código anterior define un generador llamado numberGenerator, que recibe un valor n como argumento, y luego lo define y usa como el valor límite en un bucle while. Además, define una variable llamada número y le asigna el valor cero.

Llamar al generador "instanciado" (myGenerator) con el método next() ejecuta el código del generador hasta la primera instrucción yield, que devuelve 1 en este caso.

Incluso después de devolvernos un valor, la función conserva el valor de la variable ’número’ para la próxima vez que se llame a la función y aumenta su valor en uno. Entonces, la próxima vez que se llame a esta función, continuará justo donde se quedó.

Llamar a la función dos veces más nos proporciona los siguientes 2 números en la secuencia, como se ve a continuación:

1
2
3
4
$ python generator_example_1.py
0
1
2

Si tuviéramos que llamar a este generador nuevamente, habríamos recibido una excepción StopIteration ya que se completó y regresó de su ciclo while interno.

Esta funcionalidad es útil porque podemos usar generadores para crear dinámicamente iterables sobre la marcha. Si tuviéramos que envolver myGenerator con list(), obtendríamos una matriz de números (como [0, 1, 2]) en lugar de un objeto generador, que es un poco más fácil de trabajar con en algunas aplicaciones.

La diferencia entre rentabilidad y rendimiento {#la diferencia entre rentabilidad y rendimiento}

La palabra clave return devuelve un valor de una función, momento en el que la función pierde su estado local. Por lo tanto, la próxima vez que llamemos a esa función, comenzará de nuevo desde su primera declaración.

Por otro lado, yield mantiene el estado entre las llamadas a funciones y continúa desde donde lo dejó cuando llamamos al método next() nuevamente. Entonces, si se llama a yield en el generador, entonces la próxima vez que se llame al mismo generador, volveremos inmediatamente después de la última declaración de yield.

Usando return en un Generador

Un generador puede usar una declaración return, pero solo sin un valor de retorno, que tiene la forma:

1
return

Cuando el generador encuentra la sentencia return, procede como en cualquier otra función return.

Como dice el PEP 255:

Tenga en cuenta que return significa "Terminé y no tengo nada interesante que devolver", tanto para funciones generadoras como para funciones no generadoras.

Modifiquemos nuestro ejemplo anterior agregando una cláusula if-else, que discriminará los números superiores a 20. El código es el siguiente:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# generator_example_2.py

def numberGenerator(n):
  if n < 20:
     number = 0
     while number < n:
         yield number
         number += 1
  else:
     return

print(list(numberGenerator(30)))

En este ejemplo, dado que nuestro generador no generará ningún valor, será una matriz vacía, ya que el número 30 es mayor que 20. Por lo tanto, la declaración de devolución funciona de manera similar a una declaración de ruptura en este caso.

Esto se puede ver a continuación:

1
2
$ python generator_example_2.py
[]

Si hubiésemos asignado un valor inferior a 20, los resultados habrían sido similares al primer ejemplo.

Usando next() para iterar a través de un generador

Podemos analizar los valores producidos por un generador usando el método next(), como se ve en el primer ejemplo. Este método le dice al generador que solo devuelva el siguiente valor de iterable, pero nada más.

Por ejemplo, el siguiente código imprimirá en pantalla los valores del 0 al 9.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# generator_example_3.py

def numberGenerator(n):
     number = 0
     while number < n:
         yield number
         number += 1

g = numberGenerator(10)

counter = 0

while counter < 10:
    print(next(g))
    counter += 1

El código anterior es similar a los anteriores, pero llama a cada valor generado por el generador con la función next(). Para hacer esto, primero debemos crear una instancia de un generador g, que es como una variable que mantiene el estado de nuestro generador.

Cuando se llama a la función next() con el generador como argumento, la función del generador de Python se ejecuta hasta que encuentra una sentencia yield. Luego, el valor obtenido se devuelve a la persona que llama y el estado del generador se guarda para su uso posterior.

Ejecutar el código anterior producirá el siguiente resultado:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
$ python generator_example_3.py
0
1
2
3
4
5
6
7
8
9

Nota: Sin embargo, existe una diferencia de sintaxis entre Python 2 y 3. El código anterior usa la versión de Python 3. En Python 2, next() puede usar la sintaxis anterior o la siguiente sintaxis:

1
print(g.next())

¿Qué es una expresión generadora?

Las expresiones generadoras son como lista de comprensiones, pero devuelven un generador en lugar de una lista. Fueron propuestos en PEP 289 y pasaron a formar parte de Python desde la versión 2.4.

La sintaxis es similar a la lista de comprensión, pero en lugar de corchetes, usan paréntesis.

Por ejemplo, nuestro código anterior podría modificarse usando expresiones generadoras de la siguiente manera:

1
2
3
4
# generator_example_4.py

g = (x for x in range(10))
print(list(g))

Los resultados serán los mismos que en nuestros primeros ejemplos:

1
2
$ python generator_example_4.py
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Las expresiones generadoras son útiles cuando se utilizan funciones de reducción como sum(), min() o max(), ya que reducen el código a una sola línea. También son mucho más cortos de escribir que una función completa de generador de Python. Por ejemplo, el siguiente código sumará los primeros 10 números:

1
2
3
4
# generator_example_5.py

g = (x for x in range(10))
print(sum(g))

Después de ejecutar este código, el resultado será:

1
2
$ python generator_example_5.py
45

Gestión de excepciones {#gestión de excepciones}

Una cosa importante a tener en cuenta es que la palabra clave yield no está permitida en la parte try de una construcción try/finally. Por lo tanto, los generadores deben asignar recursos con cautela.

Sin embargo, yield puede aparecer en las cláusulas finally, except o en la parte try de las cláusulas try/except.

Por ejemplo, podríamos haber creado el siguiente código:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# generator_example_6.py

def numberGenerator(n):
  try:
     number = 0
     while number < n:
         yield number
         number += 1
  finally:
     yield n

print(list(numberGenerator(10)))

En el código anterior, como resultado de la cláusula finally, el número 10 se incluye en la salida y el resultado es una lista de números del 0 al 10. Esto normalmente no sucedería ya que la declaración condicional es número < n. Esto se puede ver en la salida a continuación:

1
2
$ python generator_example_6.py
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

Envío de valores a generadores

Los generadores tienen una herramienta poderosa en el método send() para generadores-iteradores. Este método se definió en PEP 342 y está disponible desde la versión 2.5 de Python.

El método send() reanuda el generador y envía un valor que se utilizará para continuar con el siguiente rendimiento. El método devuelve el nuevo valor proporcionado por el generador.

La sintaxis es send() o send(value). Sin ningún valor, el método de envío es equivalente a una llamada next(). Este método también puede usar Ninguno como valor. En ambos casos, el resultado será que el generador avanza su ejecución a la primera expresión yield.

Si el generador sale sin generar un nuevo valor (como al usar return), el método send() genera StopIteration.

El siguiente ejemplo ilustra el uso de send(). En la primera y tercera línea de nuestro generador, le pedimos al programa que asigne a la variable número el valor que arrojó previamente. En la primera línea después de nuestra función de generador, creamos una instancia del generador y generamos un primer “rendimiento” en la siguiente línea llamando a la función “siguiente”. Así, en la última línea enviamos el valor 5, que será utilizado como entrada por el generador, y considerado como su rendimiento anterior.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# generator_example_7.py

def numberGenerator(n):
     number = yield
     while number < n:
         number = yield number 
         number += 1

g = numberGenerator(10)    # Create our generator
next(g)                    # 
print(g.send(5))

Nota: Debido a que no hay un valor obtenido cuando se crea el generador por primera vez, antes de usar send(), debemos asegurarnos de que el generador haya obtenido un valor usando next() o send(Ninguno) . En el ejemplo anterior, ejecutamos la línea next(g) por este motivo, de lo contrario obtendríamos un error que dice "Error de tipo: no se puede enviar un valor que no sea Ninguno a un generador recién iniciado" .

Luego de ejecutar el programa, imprime en pantalla el valor 5, que es el que le enviamos:

1
2
$ python generator_example_7.py
5

La tercera línea de nuestro generador desde arriba también muestra una nueva característica de Python introducida en el mismo PEP: expresiones de rendimiento. Esta característica permite que la cláusula yield se use en el lado derecho de una declaración de asignación. El valor de una expresión de rendimiento es Ninguno, hasta que el programa llame al método send(value).

Conexión de generadores

Desde Python 3.3, una nueva característica permite que los generadores se conecten y deleguen a un subgenerador.

La nueva expresión está definida en PEP 380, y su sintaxis es:

1
yield from <expression>

donde <expression> es una expresión que se evalúa como un iterable, que define el generador de delegación.

Veamos esto con un ejemplo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# generator_example_8.py

def myGenerator1(n):
    for i in range(n):
        yield i

def myGenerator2(n, m):
    for j in range(n, m):
        yield j

def myGenerator3(n, m):
    yield from myGenerator1(n)
    yield from myGenerator2(n, m)
    yield from myGenerator2(m, m+5)

print(list(myGenerator1(5)))
print(list(myGenerator2(5, 10)))
print(list(myGenerator3(0, 10)))

El código anterior define tres generadores diferentes. El primero, llamado myGenerator1, tiene un parámetro de entrada, que se usa para especificar el límite en un rango. El segundo, llamado myGenerator2, es similar al anterior, pero contiene dos parámetros de entrada, que especifican los dos límites permitidos en el rango de números. Después de esto, myGenerator3 llama a myGenerator1 y myGenerator2 para obtener sus valores.

Las tres últimas líneas de código imprimen en pantalla tres listas generadas a partir de cada uno de los tres generadores definidos anteriormente. Como podemos ver cuando ejecutamos el programa a continuación, el resultado es que myGenerator3 usa los rendimientos obtenidos de myGenerator1 y myGenerator2, para generar una lista que combina las tres listas anteriores.

El ejemplo también muestra una aplicación importante de los generadores: la capacidad de dividir una tarea larga en varias partes separadas, lo que puede ser útil cuando se trabaja con grandes conjuntos de datos.

1
2
3
4
$ python generator_example_8.py
[0, 1, 2, 3, 4]
[5, 6, 7, 8, 9]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]

Como puede ver, gracias a la sintaxis yield from, los generadores se pueden encadenar para una programación más dinámica.

Beneficios de los generadores

  1. Código simplificado

Como se ve en los ejemplos que se muestran en este artículo, los generadores simplifican el código de una manera muy elegante. Esta simplificación y elegancia del código son aún más evidentes en las expresiones del generador, donde una sola línea de código reemplaza un bloque completo de código.

  1. Mejor rendimiento

Los generadores funcionan en la generación perezosa (a pedido) de valores. Esto da como resultado dos ventajas. Primero, menor consumo de memoria. Sin embargo, este ahorro de memoria funcionará en nuestro beneficio si usamos el generador solo una vez. Si usamos los valores varias veces, puede valer la pena generarlos de una vez y guardarlos para su uso posterior.

La naturaleza bajo demanda de los generadores también significa que es posible que no tengamos que generar valores que no se usarán y, por lo tanto, se habrían desperdiciado ciclos si fueran generados. Esto significa que su programa puede usar solo los valores necesarios sin tener que esperar hasta que se hayan generado todos.

Cuándo usar generadores

Los generadores son una herramienta avanzada presente en Python. Hay varios casos de programación en los que los generadores pueden aumentar la eficiencia. Algunos de estos casos son:

  • Procesamiento de grandes cantidades de datos: los generadores proporcionan cálculo bajo demanda, también llamado evaluación perezosa. Esta técnica se utiliza en el procesamiento de flujo.
  • Tuberías: los generadores apilados se pueden usar como tuberías, de manera similar a las tuberías de Unix.
  • Concurrencia: se pueden utilizar generadores para generar (simular) concurrencia.

Finalizando

Los generadores son un tipo de función que genera una secuencia de valores. Como tales, pueden actuar de manera similar a los iteradores. Su uso da como resultado un código más elegante y un rendimiento mejorado.

Estos aspectos son aún más evidentes en las expresiones generadoras, donde una línea de código puede resumir una secuencia de declaraciones.

La capacidad de trabajo de los generadores se ha mejorado con nuevos métodos, como send(), y declaraciones mejoradas, como yield from.

Como resultado de estas propiedades, los generadores tienen muchas aplicaciones útiles, como la generación de conductos, la programación concurrente y la ayuda en la creación de flujos a partir de grandes cantidades de datos.

Como consecuencia de estas mejoras, Python se está convirtiendo cada vez más en el lenguaje elegido en la ciencia de datos.

¿Para qué has usado generadores? ¡Cuéntanos en los comentarios!