Bienvenidos sean a este post, hoy veremos uno de los dos tipos de generadores disponibles.
Como dijimos en el post anterior este tipo de funciones son muy similares a las funciones comunes pero con una diferencia, en lugar de recolectar los resultados y devolverlos todos juntos se los convierte en iteradores que producen los resultados una a la vez cuando los llama next, esto es la teoria pero antes de ver el ejemplo hablemos un poco de su mecanica.
Vamos a suponer que necesitamos contar de 1 a 1000000, comenzamos con el proceso pero en un punto necesitamos detenerlo y luego de un tiempo necesitamos retomarlo, aqui nos preguntamos cual es la informacion minima que necesitamos para retomar correctamente? Se necesita recordar el ultimo numero llamado, por ejemplo si nos detuvimos en 32876 deberemos continuar con el 32877 y asi sucesivamente, no necesitamos recordar todos los numeros anteriores a 32876 ni tampoco que sean escritos en alguna otra parte, con este razonamiento sin darte cuenta te estas comportando como un generador, vamos a mirar el siguiente ejemplo:
>>> def get_cuadrados(n):
... return [x ** 2 for x in range(n)]
...
>>>
Esta es la definicion de una funcion clasica, donde utilizaremos una comprension de tipo list para obtener los cuadrados del rango informado en n, probemos para ver como trabaja:
>>> print(get_cuadrados(10))
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
>>>
Ahora vamos a generar la siguiente funcion:
>>> def get_cuadrados_gen(n) :
... for x in range(n):
... yield x ** 2
...
>>>
Esta funcion obtiene los cuadrados pero esta vez por medio de un generador, en este caso creamos un bucle que trabaja de la misma que el anterior pero en lugar de usar un return usamos a yield, este permite que se produzca el calculo y se almacena pero no lo devuelve, para poder verlo debemos usar un list de los contrario no nos mostrara nada, veamos como es su salida:
>>> print(list(get_cuadrados_gen(10)))
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
>>> print(get_cuadrados_gen(10))
<generator object get_cuadrados_gen at 0x7f46f9642750>
>>>
Como dijimos si usamos a list obtenemos todos los datos, en cambio si lo intentamos imprimir directamente nos devuelve que es un objeto de tipo generator y la direccion de memoria donde esta ubicado, con esto aclarado vamos a hacer una prueba para ello vamos a crear la siguiente variable:
>>> cuadrados = get_cuadrados_gen(4)
Ahora tenemos todo lo producido por la funcion dentro de cuadrados, probemos de mostrar este nombre en pantalla:
>>> print(cuadrados)
<generator object get_cuadrados_gen at 0x7f46f9642750>
>>>
Nos ocurrio lo mismo que antes pero recuerdan lo que mencionamos al principio, los resultados se mostraran despues de cada llamada de next, veamos el siguiente caso:
>>> print(next(cuadrados))
0
>>>
Como pueden ver nos mostro el primer valor almacenado en cuadrados, vamos a ejecutarlo una tres veces mas:
>>> print(next(cuadrados))
1
>>> print(next(cuadrados))
4
>>> print(next(cuadrados))
9
>>>
Como pueden ver nos va pasando de un valor al siguiente pero que pasa si lo ejecutamos nuevamente?, veamos:
>>> print(next(cuadrados))
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
>>>
Como en el caso anterior alcanzamos el maximo de posiciones almacenadas cuando volvemos a ejecutarlo nos pasamos del limite de la iteracion y por lo tanto nos devuelve un StopIteration indicando que se detuvo la iteracion porque se termino y de ahora en mas nos devolvera siempre el mismo error, esto es una visualizacion de como trabaja en realidad un for, ya que cuando le decimos que haga la iteracion, la ejecuta hasta el ultimo que tiene disponible y se detiene al encontrar un StopIteration, con todo esto comentado tenemos una idea de como trabajan las funciones generadoras y ahora se preguntaran para que nos puede ser util?, hablemos un poco sobre eso.
La finalidad de una funcion generadora es poder hacer ciertas cosas que una funcion normal no podria, por ejemplo vamos a crear una funcion que analice todas las permutaciones de una secuencia, por lo tanto si la secuencia tiene una longitud N el numero de permutaciones es N!, vamos a suponer que tenemos una secuencia con una longitud de 10 el resultado seria 3628800 pero que pasa con una longitud de 20? tenemos un total de 2432902008176640000, wow que paso? Esto se debe a que crecen exponencialmente, por lo tanto si tenemos una funcion que se dedique a calcular las permutaciones, almacenarlo en una lista y por ultimo devolvertelos, con una longitud de 10 puede que lo haga en un tiempo razonable pero con la longitud de 20 seguramente no se realice, aqui podemos hacer entrar en accion a las funciones generadoras.
Siguiendo con el ejemplo si aplicamos a la funcion generadora este comenzara a procesarlo pero a diferencia del caso anterior este procesara uno por uno y si bien seguira fallando pero ahora tendremos un control para interrumpirlo y poder manejar una cierta cantidad de esta informacion, un ejemplo es cuando hablamos de break en este post donde al llegar a un valor salia del bucle, en algunas ocasiones con este tipo de funciones generaremos una condicion similar porque tendremos demasiada informacion que procesar en memoria pero por lo menos obtendremos una porcion de dicha informacion de lo contrario seria imposible.
Para entender este concepto vamos a ver el siguiente ejemplo:
>>> def progresion_geometrica(a, b):
... c = 0
... while True:
... resultado = a * b ** c
... if resultado <= 100000:
... yield resultado
... else:
... return
... c += 1
...
>>>
En este ejemplo primero definimos una funcion que recibira dos valores, luego estableceremos un nuevo nombre con el valor inicial de 0, luego tendremos un while que mientras sea true seguira corriendo, es decir que es un bucle infinito, dentro de este bucle tenemos un nombre llamado resultado el cual almacenara el resultado del valor de a por b elevado al valor contenido en c, esto equivale a una progresion geometrica: a, ab, ab², ab³ … despues tenemos un condicional donde si resultado es menor o igual a 100000 lo produce por medio de yield pero en caso de no cumplirse mas la condicion devuelve un return vacio y sale de la funcion, este sera nuestro «break» pero que no interrumpira de forma definitiva a nuestra funcion, por ultimo incrementamos a c mientras estamos dentro del bucle, probemos la funcion para ver su salida:
>>> for n in progresion_geometrica(2, 5):
... print(n)
...
2
10
50
250
1250
6250
31250
>>>
Como pueden ver mostro los resultados hasta que llegamos al siguiente valor que es 156250 superando la condicion que establecimos.
En resumen, hoy hemos visto a las funciones generadoras, hemos visto como trabajan, como podemos utilizarlas, cual es la diferencia con respecto a las funciones normales, como nos pueden beneficiar y por ultimo un ejemplo donde lo ponemos en practica, espero les haya sido util sigueme en tumblr, Twitter o Facebook para recibir una notificacion cada vez que subo un nuevo post en este blog, nos vemos en el proximo post.


Donación
Es para mantenimento del sitio, gracias!
$1.00
