Hola, mi nombre es Antonio Alfonso Martínez y en la presente ocasión me dispongo a dar unas breves pinceladas acerca del uso de los generadores en el lenguaje de programación Python.
Para empezar, diremos que los generadores constituyen un determinado tipo de estructura que, a diferencia de lo que sucede con las funciones corrientes, no nos va a devolver un valor concreto, sino un objeto iterable cuyos resultados se ven a ir generando uno a uno.
Para entender mejor la diferencia con las funciones que empleamos habitualmente vamos a crear un sencillo programa que va a generar la lista de números pares que hay entre 0 y el número que indiquemos:
Como se ve, hemos creado una función (de nombre "pares") que tomando como argumento el numero de pares que queremos hallar (variable "maximo") irá calculando los números pares (multiplicando por 2 la variable "num") e irá añadiendo el resultado a una lista (de nombre "lista_pares") que es la que finalmente devolverá de modo integro, mediante la correspondiente sentencia "return".
Hasta aquí todo correcto, no obstante, supongamos que en un primer momento, nuestro programa solo necesita hacer uso del primer valor generado (0 en el ejemplo). En este caso, estaríamos ante un supuesto de ineficiencia en el funcionamiento de nuestro programa ya que la función habría generado una lista de 10 elementos (con el correspondiente uso de memoria) de la que solo haríamos uso de uno de ellos. Tal vez en este ejemplo concreto no suponga mucho, pero imaginemos que estuviéramos manejando grandes cantidades de información. Es en este supuesto donde el empleo de generadores puede suponer el lograr una mayor eficiencia en el consumo de memoria y tiempo de ejecución:
Para ilustrar la diferencia del generador con las funciones al uso, hemos creado un generador a partir de la función anterior ("pares") en donde podemos apreciar como primera diferencia el hecho de que en este caso, vamos a prescindir de la lista "lista_pares" debido a que, como dijimos anteriormente, lo que vamos a generar aquí no es la lista integra con todos los elementos sino un objeto iterable que nos va a devolver el primero de dichos valores mediante la instrucción "yield" (a diferencia de lo que sucede con las funciones corrientes que como sabemos usan la instrucción "return" para devolvernos el valor).
Así, si ahora pasamos a visualizar el resultado de nuestra función, mediante una variable a la que hemos llamado "num_pares":
Vemos como la función nos ha devuelto el objeto iterable al que nos hemos referido con anterioridad. No obstante, cuando usemos este tipo de estructuras en un programa, lo normal, es que lo que deseemos obtener sean los valores generador con la instrucción "yield". Para poder ir visualizando tales resultados (que en nuestro ejemplo se corresponde con los 10 primeros números pares), haremos uso de la instrucción "next" del modo siguiente ("print(next(num_pares))"):
Como se ve en la imagen, al imprimir (haciendo uso del método "next") el valor de "num_pares", hemos obtenido tan solo el primero de los valores (el 0) de la lista de números pares, quedando en suspenso la generación de los restantes 9 valores, los cuales nos irán siendo devueltos uno a uno cada vez que volvamos a llamar a la función con el método "next", tal y como se aprecia en la imagen:
De este modo, nuestro hipotético programa podría ir accediendo, sucesivamente a cada uno de los elementos generados por la función, sin necesidad, en cada vez, de reservar espacio de memoria para los elementos que no van a ser usados en ese mometo concreto (reservándose espacio solo para el elemento concreto que vaya a necesitar el programa).
Visto el proceso, cabe formularse el supuesto en el que una vez que nuestro programa ha hecho uso de la totalidad de los elementos devueltos por el generador (que en nuestro ejemplo hemos ido visualizando con "print"), pretendemos reiniciar todo el proceso. En este punto, alguno podría pensar que se puede hacer haciendo, nuevamente, uso de la instrucción. No obstante si llevamos a cabo tal acción, obtendremos lo siguiente:
Como se puede ver, en ese caso se nos genera un error con el mensaje "StopIteration". Esto, lo que nos está indicando es que el proceso de generación, sucesiva de valores, en principio, solo se puede llevar a cabo una sola vez. Esto ha de ser tenido en cuenta en la medida de que, para volver a iniciar el proceso tendríamos, básicamente, 2 opciones: La primera de ellas sería, naturalmente, la de volver a definir la función con su generador. La otra, podría ser la de haber ido añadiendo cada uno de los valores generados a una lista, de forma sucesiva.
La elección entre uno de tales métodos deberá hacerse en función de las características de nuestro programa y de la cantidad de datos que estemos manejando: Así, para el caso en el que estemos ante una gran cantidad de datos, parece que el primer método (volver a definir la función) puede ser el más adecuado. Por contra si nuestros datos han sido generados por una función más compleja (y por tanto que necesite un mayor tiempo e ejecución) Parece que el segundo método (guardado de datos en lista) parece el más efectivo ya que evitará el tener que hacer uso de la función nuevamente.
En futuros artículos, seguiremos dando detalles acerca de las posibilidades y utilidades que pueden ofrecernos los generadores.
Comentarios
Publicar un comentario