Python Language
Generadores
Buscar..
Introducción
Los generadores son iteradores perezosos creados por las funciones del generador (que utilizan el yield
) o las expresiones del generador (que usan (an_expression for x in an_iterator)
).
Sintaxis
- rendimiento
<expr>
- rendimiento de
<expr>
-
<var>
= rendimiento<expr>
- siguiente (
<iter>
)
Iteración
Un objeto generador soporta el protocolo iterador . Es decir, proporciona un método next()
( __next__()
en Python 3.x), que se utiliza para avanzar en su ejecución, y su método __iter__
devuelve. Esto significa que se puede usar un generador en cualquier construcción de lenguaje que admita objetos iterables genéricos.
# naive partial implementation of the Python 2.x xrange()
def xrange(n):
i = 0
while i < n:
yield i
i += 1
# looping
for i in xrange(10):
print(i) # prints the values 0, 1, ..., 9
# unpacking
a, b, c = xrange(3) # 0, 1, 2
# building a list
l = list(xrange(10)) # [0, 1, ..., 9]
La siguiente función ()
El next()
incorporado es un envoltorio conveniente que se puede usar para recibir un valor de cualquier iterador (incluido un iterador generador) y para proporcionar un valor predeterminado en caso de que se agote el iterador.
def nums():
yield 1
yield 2
yield 3
generator = nums()
next(generator, None) # 1
next(generator, None) # 2
next(generator, None) # 3
next(generator, None) # None
next(generator, None) # None
# ...
La sintaxis es la next(iterator[, default])
. Si el iterador finaliza y se pasa un valor predeterminado, se devuelve. Si no se proporcionó ningún valor predeterminado, se StopIteration
.
Enviando objetos a un generador.
Además de recibir valores de un generador, es posible enviar un objeto a un generador utilizando el método send()
.
def accumulator():
total = 0
value = None
while True:
# receive sent value
value = yield total
if value is None: break
# aggregate values
total += value
generator = accumulator()
# advance until the first "yield"
next(generator) # 0
# from this point on, the generator aggregates values
generator.send(1) # 1
generator.send(10) # 11
generator.send(100) # 111
# ...
# Calling next(generator) is equivalent to calling generator.send(None)
next(generator) # StopIteration
Lo que pasa aquí es lo siguiente:
- Cuando llama por primera vez al
next(generator)
, el programa avanza a la primera declaración deyield
y devuelve el valor deltotal
en ese punto, que es 0. La ejecución del generador se suspende en este punto. - Cuando llama a
generator.send(x)
, el intérprete toma el argumentox
y lo convierte en el valor de retorno de la última declaración deyield
, que se asigna alvalue
. El generador continúa como de costumbre, hasta que da el siguiente valor. - Cuando finalmente llama a
next(generator)
, el programa trata esto como si estuviera enviandoNone
al generador. No hay nada especial enNone
, sin embargo, este ejemplo utilizaNone
como un valor especial para pedirle al generador que se detenga.
Expresiones generadoras
Es posible crear iteradores de generador utilizando una sintaxis similar a la comprensión.
generator = (i * 2 for i in range(3))
next(generator) # 0
next(generator) # 2
next(generator) # 4
next(generator) # raises StopIteration
Si a una función no necesariamente se le debe pasar una lista, puede guardar caracteres (y mejorar la legibilidad) colocando una expresión de generador dentro de una llamada de función. El paréntesis de la llamada a la función hace implícitamente que su expresión sea una expresión generadora.
sum(i ** 2 for i in range(4)) # 0^2 + 1^2 + 2^2 + 3^2 = 0 + 1 + 4 + 9 = 14
Además, guardará en la memoria porque en lugar de cargar la lista completa sobre la que está iterando ( [0, 1, 2, 3]
en el ejemplo anterior), el generador permite que Python use los valores según sea necesario.
Introducción
Las expresiones de los generadores son similares a las listas, diccionarios y conjuntos de comprensión, pero están entre paréntesis. Los paréntesis no tienen que estar presentes cuando se usan como el único argumento para una llamada de función.
expression = (x**2 for x in range(10))
Este ejemplo genera los 10 primeros cuadrados perfectos, incluido 0 (en el que x = 0).
Las funciones del generador son similares a las funciones regulares, excepto que tienen una o más declaraciones de yield
en su cuerpo. Dichas funciones no pueden return
ningún valor (sin embargo, se permiten return
vacías si desea detener el generador antes).
def function():
for x in range(10):
yield x**2
Esta función del generador es equivalente a la expresión del generador anterior, produce el mismo.
Nota : todas las expresiones generadoras tienen sus propias funciones equivalentes , pero no al revés.
Se puede usar una expresión generadora sin paréntesis si ambos paréntesis se repetirían de lo contrario:
sum(i for i in range(10) if i % 2 == 0) #Output: 20
any(x = 0 for x in foo) #Output: True or False depending on foo
type(a > b for a in foo if a % 2 == 1) #Output: <class 'generator'>
En lugar de:
sum((i for i in range(10) if i % 2 == 0))
any((x = 0 for x in foo))
type((a > b for a in foo if a % 2 == 1))
Pero no:
fooFunction(i for i in range(10) if i % 2 == 0,foo,bar)
return x = 0 for x in foo
barFunction(baz, a > b for a in foo if a % 2 == 1)
Al llamar a una función de generador se genera un objeto generador , que luego se puede iterar. A diferencia de otros tipos de iteradores, los objetos generadores solo se pueden atravesar una vez.
g1 = function()
print(g1) # Out: <generator object function at 0x1012e1888>
Observe que el cuerpo de un generador no se ejecuta inmediatamente: cuando llama a function()
en el ejemplo anterior, devuelve inmediatamente un objeto generador, sin ejecutar siquiera la primera declaración de impresión. Esto permite que los generadores consuman menos memoria que las funciones que devuelven una lista, y permite crear generadores que producen secuencias infinitamente largas.
Por esta razón, los generadores a menudo se utilizan en la ciencia de datos y en otros contextos que involucran grandes cantidades de datos. Otra ventaja es que otro código puede usar inmediatamente los valores generados por un generador, sin esperar a que se produzca la secuencia completa.
Sin embargo, si necesita usar los valores producidos por un generador más de una vez, y si generarlos cuesta más que almacenarlos, puede ser mejor almacenar los valores generados como una list
que volver a generar la secuencia. Consulte 'Restablecer un generador' a continuación para obtener más detalles.
Normalmente, un objeto generador se usa en un bucle, o en cualquier función que requiera un iterable:
for x in g1:
print("Received", x)
# Output:
# Received 0
# Received 1
# Received 4
# Received 9
# Received 16
# Received 25
# Received 36
# Received 49
# Received 64
# Received 81
arr1 = list(g1)
# arr1 = [], because the loop above already consumed all the values.
g2 = function()
arr2 = list(g2) # arr2 = [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
Como los objetos generadores son iteradores, uno puede recorrerlos manualmente usando la función next()
. Al hacerlo, se devolverán los valores cedidos uno por uno en cada invocación posterior.
Bajo el capó, cada vez que llama a next()
en un generador, Python ejecuta declaraciones en el cuerpo de la función del generador hasta que llega a la siguiente declaración de yield
. En este punto, devuelve el argumento del comando de yield
y recuerda el punto en el que ocurrió. Llamar a next()
una vez más reanudará la ejecución desde ese punto y continuará hasta la próxima declaración de yield
.
Si Python llega al final de la función del generador sin encontrar más yield
, se StopIteration
una excepción StopIteration
(esto es normal, todos los iteradores se comportan de la misma manera).
g3 = function()
a = next(g3) # a becomes 0
b = next(g3) # b becomes 1
c = next(g3) # c becomes 2
...
j = next(g3) # Raises StopIteration, j remains undefined
Tenga en cuenta que en el generador Python 2, los objetos tenían métodos .next()
que se podían usar para iterar a través de los valores producidos manualmente. En Python 3, este método fue reemplazado por el estándar .__next__()
para todos los iteradores.
Restablecer un generador
Recuerde que solo puede recorrer los objetos generados por un generador una vez . Si ya ha iterado a través de los objetos en una secuencia de comandos, cualquier otro intento de hacerlo dará como resultado None
.
Si necesita usar los objetos generados por un generador más de una vez, puede definir la función del generador de nuevo y usarla por segunda vez, o bien, puede almacenar la salida de la función del generador en una lista en el primer uso. Volver a definir la función del generador será una buena opción si está manejando grandes volúmenes de datos, y almacenar una lista de todos los elementos de datos ocuparía mucho espacio en el disco. A la inversa, si es costoso generar los artículos inicialmente, es posible que prefiera almacenar los artículos generados en una lista para poder reutilizarlos.
Usando un generador para encontrar los números de Fibonacci
Un caso de uso práctico de un generador es recorrer los valores de una serie infinita. Aquí hay un ejemplo de cómo encontrar los primeros diez términos de la secuencia de Fibonacci .
def fib(a=0, b=1):
"""Generator that yields Fibonacci numbers. `a` and `b` are the seed values"""
while True:
yield a
a, b = b, a + b
f = fib()
print(', '.join(str(next(f)) for _ in range(10)))
0, 1, 1, 2, 3, 5, 8, 13, 21, 34
Secuencias infinitas
Se pueden usar generadores para representar secuencias infinitas:
def integers_starting_from(n):
while True:
yield n
n += 1
natural_numbers = integers_starting_from(1)
La secuencia infinita de números como la anterior también se puede generar con la ayuda de itertools.count
. El código anterior se puede escribir como abajo
natural_numbers = itertools.count(1)
Puede usar la comprensión del generador en generadores infinitos para producir nuevos generadores:
multiples_of_two = (x * 2 for x in natural_numbers)
multiples_of_three = (x for x in natural_numbers if x % 3 == 0)
Tenga en cuenta que un generador infinito no tiene un fin, por lo que pasarlo a cualquier función que intente consumir el generador por completo tendrá graves consecuencias :
list(multiples_of_two) # will never terminate, or raise an OS-specific error
En su lugar, use la lista de listas / conjuntos con range
(o xrange
para python <3.0):
first_five_multiples_of_three = [next(multiples_of_three) for _ in range(5)]
# [3, 6, 9, 12, 15]
o use itertools.islice()
para cortar el iterador en un subconjunto:
from itertools import islice
multiples_of_four = (x * 4 for x in integers_starting_from(1))
first_five_multiples_of_four = list(islice(multiples_of_four, 5))
# [4, 8, 12, 16, 20]
Tenga en cuenta que el generador original también se actualiza, al igual que todos los demás generadores que vienen de la misma "raíz":
next(natural_numbers) # yields 16
next(multiples_of_two) # yields 34
next(multiples_of_four) # yields 24
Una secuencia infinita también se puede iterar con un bucle for
. Asegúrese de incluir una instrucción de break
condicional para que el bucle finalice eventualmente:
for idx, number in enumerate(multiplies_of_two):
print(number)
if idx == 9:
break # stop after taking the first 10 multiplies of two
Ejemplo clásico - números de Fibonacci
import itertools
def fibonacci():
a, b = 1, 1
while True:
yield a
a, b = b, a + b
first_ten_fibs = list(itertools.islice(fibonacci(), 10))
# [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
def nth_fib(n):
return next(itertools.islice(fibonacci(), n - 1, n))
ninety_nineth_fib = nth_fib(99) # 354224848179261915075
Rindiendo todos los valores de otro iterable.
Utilice el yield from
si desea obtener todos los valores de otro iterable:
def foob(x):
yield from range(x * 2)
yield from range(2)
list(foob(5)) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1]
Esto funciona también con generadores.
def fibto(n):
a, b = 1, 1
while True:
if a >= n: break
yield a
a, b = b, a + b
def usefib():
yield from fibto(10)
yield from fibto(20)
list(usefib()) # [1, 1, 2, 3, 5, 8, 1, 1, 2, 3, 5, 8, 13]
Coroutines
Los generadores pueden ser utilizados para implementar coroutines:
# create and advance generator to the first yield
def coroutine(func):
def start(*args,**kwargs):
cr = func(*args,**kwargs)
next(cr)
return cr
return start
# example coroutine
@coroutine
def adder(sum = 0):
while True:
x = yield sum
sum += x
# example use
s = adder()
s.send(1) # 1
s.send(2) # 3
Coroutines se usa comúnmente para implementar máquinas de estado, ya que son principalmente útiles para crear procedimientos de un solo método que requieren un estado para funcionar correctamente. Operan en un estado existente y devuelven el valor obtenido al finalizar la operación.
Rendimiento con recursión: listado recursivo de todos los archivos en un directorio
Primero, importa las bibliotecas que trabajan con archivos:
from os import listdir
from os.path import isfile, join, exists
Una función auxiliar para leer solo archivos de un directorio:
def get_files(path):
for file in listdir(path):
full_path = join(path, file)
if isfile(full_path):
if exists(full_path):
yield full_path
Otra función auxiliar para obtener solo los subdirectorios:
def get_directories(path):
for directory in listdir(path):
full_path = join(path, directory)
if not isfile(full_path):
if exists(full_path):
yield full_path
Ahora use estas funciones para obtener recursivamente todos los archivos dentro de un directorio y todos sus subdirectorios (usando generadores):
def get_files_recursive(directory):
for file in get_files(directory):
yield file
for subdirectory in get_directories(directory):
for file in get_files_recursive(subdirectory): # here the recursive call
yield file
Esta función se puede simplificar utilizando el yield from
:
def get_files_recursive(directory):
yield from get_files(directory)
for subdirectory in get_directories(directory):
yield from get_files_recursive(subdirectory)
Iterando sobre generadores en paralelo.
Para iterar sobre varios generadores en paralelo, use el zip
incorporado:
for x, y in zip(a,b):
print(x,y)
Resultados en:
1 x
2 y
3 z
En Python 2 deberías usar itertools.izip
en itertools.izip
lugar. Aquí también podemos ver que todas las funciones zip
producen tuplas.
Tenga en cuenta que zip dejará de iterar tan pronto como uno de los iterables se quede sin elementos. Si desea iterar durante el tiempo más largo posible, use itertools.zip_longest()
.
Código de construcción de lista de refactorización
Supongamos que tiene un código complejo que crea y devuelve una lista al comenzar con una lista en blanco y agregarla repetidamente:
def create():
result = []
# logic here...
result.append(value) # possibly in several places
# more logic...
return result # possibly in several places
values = create()
Cuando no es práctico reemplazar la lógica interna con una lista de comprensión, puede convertir toda la función en un generador en el lugar y luego recopilar los resultados:
def create_gen():
# logic...
yield value
# more logic
return # not needed if at the end of the function, of course
values = list(create_gen())
Si la lógica es recursiva, use el yield from
para incluir todos los valores de la llamada recursiva en un resultado "aplanado":
def preorder_traversal(node):
yield node.value
for child in node.children:
yield from preorder_traversal(child)
buscando
La next
función es útil incluso sin iterar. Pasar una expresión del generador a la next
es una forma rápida de buscar la primera aparición de un elemento que coincida con algún predicado. Código de procedimiento como
def find_and_transform(sequence, predicate, func):
for element in sequence:
if predicate(element):
return func(element)
raise ValueError
item = find_and_transform(my_sequence, my_predicate, my_func)
puede ser reemplazado con:
item = next(my_func(x) for x in my_sequence if my_predicate(x))
# StopIteration will be raised if there are no matches; this exception can
# be caught and transformed, if desired.
Para este propósito, puede ser conveniente crear un alias, como first = next
, o una función de envoltura para convertir la excepción:
def first(generator):
try:
return next(generator)
except StopIteration:
raise ValueError