Generadores en Python

Si algo tiene de bueno aprender un lenguaje de script es que descubres nuevas maneras para hacer las cosas que un lenguaje tradicional como C o Java habitualmente no te ofrece.

En el caso de Python, existen múltiples cosas a las que los programadores no estamos acostumbrados, en este post concretamente vamos a hablar de los generadores.

Desde un punto de vista muy básico, un generador es un método para extraer los resultados de una función de una forma diferente a lo que tenemos acostumbrado a usar. Es un concepto mas cercano a la programacion funcional de lisp o haskell que a la programación procedimental de C o Pascal.

Normalmente lo usaremos cuando los datos que una funcion devuelve es una lista de datos. El enfoque tradicional sería construir la lista en la función y devolverla tal cual al llamador, pero esta forma de funcionar tiene alguna limitación.
Por ejemplo, si el proceso de recolectar la lista es muy costoso, el llamador no obtendrá ningún resultado hasta el final del proceso, y si para mas añadido, el llamador puede no necesitar todos los elementos en función de algún elemento de la lista; usando la forma típica de procesar este comportamiento, el llamador no tiene manera de obtener parcialmente esos resultados.

Además desde el punto de vista de la eficiencia, si la lista es muy grande, la estamos iterando dos veces, una al generarla y otra al procesarla.

Aparte de este uso, es frecuente encontrarse con funciones en que queremos ir extrayendo poco a poco resultados, el funcionamiento típico se basa en llamar repetidas veces a la función con el «estado» de dicha función pasado por parámetros desde el llamador, por ejemplo, en las típicas funciones que te buscan caracteres en una cadena de forma progresiva, cada vez que quieres un nuevo resultado, necesitas pasarle la cadena recortada hasta la anterior ocurrencia del caracter que buscamos. Con los generadores podremos hacer esto de una forma mucho mas natural.

Realmente esta forma de funcionar, no es que no se pueda hacer con C, por ejemplo, usando callbacks podemos simular el mismo comportamiento, pero sin duda la forma de afrontarlo en Python, es mucho mas sencilla, ya que entre otras cosas, está integrado como mecanismo genérico del lenguaje.

Como siempre la mejor forma de aprender algo es viendo un ejemplo sobre ello. Usando los generadores vamos a desarrollar una función que pasado un número binario lo irá «desgranando» y devolviendo los valores decimales por cada bit.

Esto es, si le pasamos a la funcion 10011, la salida será 1+2+0+0+16=19.

Si esto lo hicieramos de la forma habitual, la función tendría que devolver una lista con los elementos, lista que luego deberiamos iterar de nuevo para imprimir el resultado.

### EJEMPLO SIN USAR GENERADORES
def binToDecClassic(val):
	n = 0
	res = []
	while val > 0:
		res += [(val%2) * (2**n)]
		val /= 10
		n+=1

	return res

sum = 0
out = binToDecClassic(10011)
for val in out:
	print "+", val,
	sum += val

print "=", sum

Si usamos generadores, el cógido sería:

### EJEMPLO USANDO GENERADORES
def binToDecUsingGenerators(val):
	n = 0
	while val > 0:
		yield (val % 2) * (2**n)
		val = val / 10
		n += 1

sum = 0
for val in binToDecUsingGenerators(10011):
	print "+", val ,
	sum += val

print "=", sum

Como vemos, el código es muy similar, de hecho, la sintaxis puede llevar a confusión, ya que aunque vemos dos bucles, realmente sólo hay uno, en cada parada del for se corresponde con la instruccion «yield» dentro de la función. Esta palabra reservada, «yield» es la clave de los generadores. Cuando ponemos yield, devolvemos el control al llamador, con el resultado de la expresión que acompaña a dicha palabra, podemos decir que es como un return, pero que no finaliza definitivamente la función.

Este ejemplo, dada su sencillez para ver como funciona un generador, no aprovecha las ventajas que hemos comentado anteriormente, para ello, vamos a ver otro ejemplo algo más completo, para ello vamos a usar la funcion walk() del paquete estandar os. Esta función, dada un directorio raíz, va devolviendo poco a poco, usando «yield» los ficheros y subdirectorios de cada directorio. Por ejemplo, tenemos una función «searchInPath» que busca un fichero en un path:

import os

def searchInPath(path, filename):
    cont = 0
    for root, dirs, files in os.walk(path):
        if filename in files:
            return cont, True
        cont += 1
    return cont, False

[cont, found] = searchInPath('c:\\temp', 'prueba.py')
print cont,' steps - found: ', found

En cada directorio, se nos devolverá el control, de tal manera que si encontramos el fichero, no seguimos iterando por el árbol de directorios, si esto mismo lo haríamos de la forma tradicional, iterariamos por todo el árbol de directorios, devolveriamos la lista y luego deberíamos buscar en la lista de ficheros.

1 Responses to Generadores en Python

  1. Luis dice:

    Buena explicacion amigo, ya entendi al extraño yield

Deja un comentario