Generadores

¿Qué son y para qué sirven?

Los generadores son estructuras que extraen valores de una función y los almacenan en objetos iterables, las funciones operadas con generadores devuelven un objeto del tipo generador el cual contiene el primer valor generado y mantiene los datos de generación de la función en memoria devolviendo el control de flujo al punto desde donde fue llamado. Los valores generados se almacenan de uno en uno en el generador, y mientras se genera el siguiente elemento el generador queda en pausa o suspensión de estado.

Dentro de las ventajas que podemos encontrar con el uso de generadores esta la eficiencia, ahorrando memoria, tiempo de proceso y uso de recursos, ya que no se está generando una lista completa para procesar tan sólo un elemento puntual en un momento determinado. Dependiendo de la circunstancia es posible que sea mas conveniente usar un generador como por ejemplo el uso de listas de valores infinitos o el procesamiento de elementos de una lista de uno en uno.

La sintaxis de un generador es básicamente igual a la declaración de una función definida por el usuario, salvo que en lugar de RETURN, se usa YIELD en líneas generales.La instrucción YIELD crea un objeto tipo generador iterable con los valores de la lista almacenados de uno en uno.

Veamos el siguiente ejemplo del uso de un generador, en principio vamos a crear una función regular que genera una lista de números pares y luego vamos a usar un generador para hacer la misma función y poder observar las diferencias entre ambos:

import os
os.system('cls')

def numeros_pares(tope):
    lListaNumeros= []
    iContador=2
    while iContador<tope:
        lListaNumeros.append(iContador)
        iContador+=2
    return lListaNumeros

print('* * * GENERADOR DE NUMEROS PARES * * *')
print(numeros_pares(int(input('Introduzca el límite superior de la lista: '))))

Resultado ingresando 11:

* * * GENERADOR DE NUMEROS PARES * * *
Introduzca el límite superior de la lista: 11
[2, 4, 6, 8, 10]

Como podemos comprobar la función cumple su cometido al recibir un valor tope a fin de no generar números hasta el infinito, luego crea una lista llamada lListaNumeros en la cual se van agregando valores hasta que se llegue al tope ingresado por el usuario, al final devuelve la lista al programa principal a través de la instrucción RETURN en donde con el uso de la función PRINT() se muestra por pantalla el resultado obtenido.

Ahora si cambiamos la instrucción RETURN por YIELD el código quedaría de la siguiente manera:

import os
os.system('cls')

def numeros_pares(tope):
    iContador = 0
    while iContador<tope:
        yield iContador
        iContador+=2

print('* * * GENERADOR DE NUMEROS PARES CON YIELD * * *')
print(numeros_pares(int(input('Introduzca el límite superior de la lista: '))))

Resultado ingresando 11:

* * * GENERADOR DE NUMEROS PARES CON YIELD * * *
Introduzca el límite superior de la lista: 11
<generator object numeros_pares at 0x000001CB579AE490>

Como podemos ver ahora Python devuelve la dirección de memoria del objeto iterable creado, pero no los datos. Para poder ver los datos es necesario ejecutar el código de la siguiente manera:

def numeros_pares(tope):
    iContador = 0
    while iContador<tope:
        yield iContador
        iContador+=2

print('* * * GENERADOR DE NUMEROS PARES CON YIELD * * *')
pares_devueltos = numeros_pares(11)

for i in pares_devueltos:
    print(i)

Resultado ingresando 11:

* * * GENERADOR DE NUMEROS PARES CON YIELD * * *
0
2
4
6
8
10

Como podemos ver aparentemente el resultado es el mismo, y el código es muy parecido al del uso de una función tradicional, y no parece que haya un gran ahorro de recursos con el uso de un generador, pero supongamos que la lista no es de 11 como tope sino de 10000 y necesitamos imprimir por pantala los tres primeros elementos de la lista. Sería muy poco práctico generar toda la lista de 10000 elementos para solamente imprimir los tres primeros elemento de la lista. Para ello lo mas práctico sería utilizar un generador y en lugar de recorrer el objeto iterable generador de principio a fin, pedirle al objeto generador que devuelva valor a valor los que el objeto generador tenga dentro. Veamos en el siguiente ejemplo como se haría.

import os
os.system('cls')

def numeros_pares(tope):
    iContador = 0
    while iContador<tope:
        yield iContador
        iContador+=2

print('* * * GENERADOR DE NUMEROS PARES CON YIELD * * *')
pares_devueltos = numeros_pares(10000)

print('Primer elemento del objeto generador')
print(next(pares_devueltos))

print('Segundo elemento del objeto generador')
print(next(pares_devueltos))

print('Tercer elemento del objeto generador')
print(next(pares_devueltos))

print(type(pares_devueltos))

Resultado:

* * * GENERADOR DE NUMEROS PARES CON YIELD * * *
Primer elemento del objeto generador
0
Segundo elemento del objeto generador
2
Tercer elemento del objeto generador
4
<class 'generator'>

Como podemos comprobar el código ha creado un objeto tipo generador el cual entrega el siguiente valor que contiene en su interior por demanda, a través de la función NEXT(). El tiempo de procesamiento de este código es considerablemente mas bajo gracias al uso de un generador en lugar de procesar una tupla entera de valores. Entre llamada y llamada al objeto generador, éste entra en un estado de stand by o suspensión.

YIELD FROM

La sintaxis YIELD FROM permite encadenar generadores, es muy similar a un arreglo de 2 dimensiones, o lo que es lo mismo generadores que estan contenidos dentro de otros generadores y su utilidad es la simplificación del código en caso de usar bucles anidados para acceder a generadores contenidos dentro de otros generadores. Veamos un ejemplo de su funcionamiento para entender mejor el concepto. Para empezar vamos a ver un ejemplo de como sería el ciclo anidado a través del uso de la instrucción FOR:

def devuelve_ciudades(*ciudades):
    for ciudad in ciudades:
        for letra in ciudad:
            yield letra

lista_ciudades=devuelve_ciudades('Caracas','Barcelona','Valencia','Maracay','Barquisimeto')

for i in range(0,10):
    print(next(lista_ciudades))

Resultado:

C
a
r
a
c
a
s
B
a
r

Nota: Recordemos que el operador * le indica a la función que va a recibir una tupla con argumentos indefinidos y será desempaquetada en el parámetro de la función.

Como podemos comprobar a través de bucles FOR anidados hemos accedido a cada caracter que compone las palabras de las ciudades. Tal como lo solicitó el bucle FOR al generar un rango de 0 a 20, se ha accedido a los primeros 20 sub elementos que componen el contenido del objeto generador, en este caso ha sido letra por letra. Sin embargo con el uso de la sintaxis <YIELD FROM> se simplificaría mucho el procedimiento antes visto, solamente habría que eliminar el bucle FOR interno <FOR letra IN ciudad> y sustituir la sintaxis <YIELD letra> por <YIELD FROM ciudad>. Python entiende que al recibir a sintaxis <YIELD FROM ciudad> debe acceder a cada sub elemento que compone los elementos del generador pero sin la complejidad de un bucle FOR anidado. El código quedaría como sigue:

def devuelve_ciudades(*ciudades):
    for ciudad in ciudades:
        yield from ciudad

lista_ciudades=devuelve_ciudades('Caracas','Barcelona','Valencia','Maracay','Barquisimeto')

for i in range(0,10):
    print(next(lista_ciudades))

Resultado:

C
a
r
a
c
a
s
B
a
r

Como podemos comprobar el resultado es exactamente el mismo pero esta vez se ha simplificado el código gracias al uso de la sintaxis YIELD FROM.

Comentarios

Entradas populares