Generadores de Python

    ¿Qué es un generador?

    Una python generador es una función que produce una secuencia de resultados. Funciona manteniendo su estado local, de modo que la función pueda reanudarse de nuevo exactamente donde se detuvo cuando se 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 yield, que tiene la siguiente sintaxis:

    yield [expression_list]
    

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

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

    ¿Cómo funcionan los generadores de Python?

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

    # 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 lo usa como valor límite en un ciclo while. Además, define una variable llamada number y le asigna el valor cero.

    Llamar al generador “instanciado” (myGenerator) con el next() El método ejecuta el código del generador hasta el primer yield declaración, que devuelve 1 en este caso.

    Incluso después de devolvernos un valor, la función mantiene el valor de la variable number para la próxima vez se llama 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:

    $ python generator_example_1.py
    0
    1
    2
    

    Si tuviéramos que llamar a este generador de nuevo, habríamos recibido un StopIteration excepción ya que se había completado y regresado de su ciclo interno while.

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

    La diferencia entre rendimiento 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 otra parte, yield mantiene el estado entre llamadas a funciones, y se reanuda desde donde lo dejó cuando llamamos al next() método de nuevo. Así que si yield se llama en el generador, luego, la próxima vez que se llame al mismo generador, lo recuperaremos después de la última yield declaración.

    Usando retorno en un generador

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

    return
    

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

    Como dice el PEP 255:

    Tenga en cuenta que return significa “He terminado y no tengo nada interesante que devolver”, tanto para las funciones generadoras como para las 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:

    # 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 producirá ningún valor, será una matriz vacía, ya que el número 30 es mayor que 20. Por lo tanto, la instrucción return funciona de manera similar a una instrucción break en este caso.

    Esto se puede ver a continuación:

    $ python generator_example_2.py
    []
    

    Si hubiéramos asignado un valor menor que 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 next() método, como se ve en el primer ejemplo. Este método le dice al generador que solo devuelva el siguiente valor del iterable, pero nada más.

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

    # 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 de arriba es similar a los anteriores, pero llama a cada valor producido por el generador con la función next(). Para hacer esto, primero debemos instanciar un generador g, que es como una variable que mantiene nuestro estado generador.

    Cuando la función next() se llama con el generador como argumento, la función del generador de Python se ejecuta hasta que encuentra un yield declaración. 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:

    $ 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 Python 3. En Python 2, el next() puede usar la sintaxis anterior o la siguiente sintaxis:

    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 las listas por comprensión, pero en lugar de corchetes, usan paréntesis.

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

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

    $ 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 de generador de Python completa. Por ejemplo, el siguiente código sumará los primeros 10 números:

    # generator_example_5.py
    
    g = (x for x in range(10))
    print(sum(g))
    

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

    $ python generator_example_5.py
    45
    

    Gestión de excepciones

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

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

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

    # 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 finally cláusula, 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 number < n. Esto se puede ver en el resultado a continuación:

    $ 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 send() método para generadores-iteradores. Este método se definió en PEP 342 y está disponible desde la versión 2.5 de Python.

    los send() El método reanuda el generador y envía un valor que se utilizará para continuar con el siguiente yield. 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 un next() llamada. Este método también puede utilizar None como valor. En ambos casos, el resultado será que el generador avance su ejecución al primer yield expresión.

    Si el generador sale sin producir un nuevo valor (como al usar return), la send() método aumenta 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 la variable number el valor anteriormente cedido. En la primera línea después de nuestra función de generador, instanciamos el generador y generamos una primera yield en la siguiente línea llamando al next función. Así, en la última línea enviamos el valor 5, que será utilizado como entrada por el generador, y considerado como su rendimiento anterior.

    # 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: Porque no hay ningún valor obtenido cuando se crea el generador por primera vez, antes de usar send(), debemos asegurarnos de que el generador arrojó un valor usando next() o send(None). En el ejemplo anterior, ejecutamos el next(g) line por esta razón, de lo contrario, obtendríamos un error que diga “TypeError: no se puede enviar un valor que no sea Ninguno a un generador recién iniciado”.

    Después de ejecutar el programa, imprime en pantalla el valor 5, que es lo que le enviamos:

    $ python generator_example_7.py
    5
    

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

    Conexión de generadores

    Desde Python 3.3, una nueva característica permite a los generadores conectarse y delegar en un subgenerador.

    La nueva expresión se define en PEP 380 y su sintaxis es:

    yield from <expression>
    

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

    Veamos esto con un ejemplo:

    # 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 utiliza 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 este, myGenerator3 llamadas myGenerator1 y myGenerator2 para ceder sus valores.

    Las últimas tres líneas de código imprimen en pantalla tres listas generadas a partir de cada uno de los tres generadores previamente definidos. Como podemos ver cuando ejecutamos el programa a continuación, el resultado es que myGenerator3 utiliza los rendimientos obtenidos de myGenerator1 y myGenerator2, para generar una lista que combine 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.

    $ 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 yield from sintaxis, los generadores se pueden encadenar juntos para una programación más dinámica.

    Beneficios de los generadores

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

    • Mejor presentación

    Los generadores trabajan en la generación perezosa (bajo demanda) 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 que valga 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 utilizarán y, por lo tanto, se habrían desperdiciado ciclos si se hubieran generado. Esto significa que su programa puede usar solo los valores necesarios sin tener que esperar hasta que se hayan generado todos.

    Cuando 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álculos a pedido, también llamados evaluación diferida. Esta técnica se utiliza en el procesamiento de flujos.
    • Tuberías: los generadores apilados se pueden usar como tuberías, de manera similar a las tuberías Unix.
    • Concurrencia: los generadores se pueden utilizar para generar (simular) la concurrencia.

    Terminando

    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 generar conductos, programación concurrente y ayudar a crear flujos a partir de grandes cantidades de datos.

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

    ¿Para qué ha utilizado los generadores? ¡Háznoslo saber en los comentarios!

     

    Etiquetas:

    Deja una respuesta

    Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *