12. Generadores - manuelmarinduque/Apuntes_Python_3 GitHub Wiki
Tomado de Generadores I. Vídeo 19 Fuente complemento
def nombre_generador(parametros):
# Cuerpo de la función generador
yield(expresión)
objeto_generador = nombre_generador(parametros)
# Para visualizar:
print(next(objeto_generador))
Un generador es un tipo de función que permite obtener sus resultados de uno en uno, es decir, cada vez que se llama a la función retorna un nuevo resultado. Un generador, a diferencia de una función corriente, no devuelve un valor concreto sino un objeto generador iterable, que con el método next() se obtienen sucesivamente sus elementos construyéndose así el generador. En cada llamada al método next() se obtiene un sólo resultado, quedando pausado la generación de los restantes, los cuales irán siendo obtenidos de uno en uno cada vez que se vuelva a llamar al generador sin necesidad de reservar espacio de memoria para los resultados que no van a ser usados en ese momento (reservándose espacio sólo para el elemento concreto que vaya a necesitar el programa).
Una característica importante de los generadores es que tanto las variables locales como el punto de inicio de la ejecución se guardan automáticamente entre las llamadas sucesivas que se hagan al generador con el método next(), es decir, a diferencia de una función común, una nueva llamada a un generador no inicia la ejecución al principio de la función, sino que la reanuda inmediatamente después del punto donde se encuentre la declaración yield (que es donde terminó la función en la última llamada).
El proceso de generación sucesiva de valores, en principio, solo se puede llevar a cabo una sola vez. Por tanto, si el programa ha hecho uso de la totalidad de los elementos devueltos por el generador y se hace un nuevo llamado, se genera el error: "StopIteration".
Para volver a iniciar el proceso se tendría básicamente 2 opciones: La primera de ellas sería volver a definir el generador. La segunda podría ser la de haber ido añadiendo cada uno de los valores generados a una lista (mediante el método append()). Esta segunda opción también resulta útil para manipular posterioremente los elementos del objeto iterable, pues en este no se puede acceder a un valor concreto con la nomenclatura: objeto iterable[?].
-
Son más eficientes computacionalmente que las funciones tradicionales, consumiendo menos recursos de memoria y tiempo de ejecución.
-
Muy útiles con listas de valores infinitos. Por ejemplo la de obtener todos los números pares.
-
Bajo determinados escenarios, será muy útil que un generador devuelva los valores de uno en uno.
Nota: Por este motivo, utilizar range es más lento que usar xrange. range genera todos los valores del rango y los devuelve en un array. En cambio, xrange genera cada valor del rango cuando se le solicita.
Para pasar una lista, tupla o String a un objeto iterable y aplicar sus funcionalidades se usa el método iter(estructura).
def pares(maximo):
num = 0
while num < maximo:
yield num*2
num += 1
num_pares = pares(5)
print(next(num_pares)) #Salida: 0
print(next(num_pares)) #Salida: 2
print(next(num_pares)) #Salida: 4
print(next(num_pares)) #Salida: 6
print(next(num_pares)) #Salida: 8
print(next(num_pares)) #Salida: “StopIteration“
Con un bucle for se generan todos los elementos del generador, por tanto, este no debe contener bucles infinitos.
for i in num_pares:
print(i)
Con una lista de valores infinitos, una función corriente no se podría emplear para generar todos los números pares; en cambio con un generador sí:
def pares():
num = 0
while True:
yield num*2
num += 1
lista_pares= pares()
pares=[]
pares.append(next(lista_pares))
pares.append(next(lista_pares))
pares.append(next(lista_pares))
pares.append(next(lista_pares))
pares.append(next(lista_pares))
pares.append(next(lista_pares))
pares.append(next(lista_pares))
pares.append(next(lista_pares))
pares.append(next(lista_pares))
print(lista_pares)
print(type(lista_pares))
print(type(pares))
Su utilidad es simplificar el código de los generadores en el caso de utilizar bucles anidados.
Esto se emplea cuando los elementos del objeto iterable son listas, tuplas, diccionarios o Strings y se quiere acceder a un segundo nivel, a sus subelementos. Normalmente en este caso se utilizaría un doble for, pero resulta sencillo al usar yield from.
Generador que devuelve una a una las ciudades de la tupla parámetro:
def devuelve_ciudades(*ciudades):
for elemento in ciudades:
yield elemento
ciudades_devueltas = devuelve_ciudades("Tuluá", "Cali", "Bogotá")
print(next(ciudades_devueltas))
print(next(ciudades_devueltas))
Ahora se quiere acceder a un segundo nivel, a los caracteres de cada String. Se puede realizar el mismo proceso mediante un doble for, pero es más simple usar yield from:
def devuelve_ciudades2(*ciudades):
for elemento in ciudades:
for subElemento in elemento:
yield subElemento
ciudades_devueltas2 = devuelve_ciudades2("Tuluá", "Cali", "Bogotá")
print(next(ciudades_devueltas2))
print(next(ciudades_devueltas2))
# Ahora con yield from:
def devuelve_ciudades3(*ciudades):
for elemento in ciudades:
yield from elemento
ciudades_devueltas3 = devuelve_ciudades3("Tuluá", "Cali", "Bogotá")
print(next(ciudades_devueltas3))
print(next(ciudades_devueltas3))
print(next(ciudades_devueltas3))
print(next(ciudades_devueltas3))
print(next(ciudades_devueltas3))
print(next(ciudades_devueltas3))
print(next(ciudades_devueltas3))
print(next(ciudades_devueltas3))
print(next(ciudades_devueltas3))
print(next(ciudades_devueltas3))
print(next(ciudades_devueltas3))
print(next(ciudades_devueltas3))
print(next(ciudades_devueltas3))
print(next(ciudades_devueltas3))
print(next(ciudades_devueltas3))