used - run generator python
Python ''rendimiento de'', o devolver un generador? (3)
Escribí esta simple pieza de código:
def mymap(func, *seq):
return (func(*args) for args in zip(*seq))
¿Debo usar la declaración de ''retorno'' como se indicó anteriormente para devolver un generador, o usar una instrucción de ''rendimiento de'' como esta:
def mymap(func, *seq):
yield from (func(*args) for args in zip(*seq))
y más allá de la diferencia técnica entre ''retorno'' y ''rendimiento desde'', ¿cuál es el mejor enfoque en el caso general?
La diferencia es que su primer mymap
es solo una función habitual, en este caso una fábrica que devuelve un generador. Todo lo que está dentro del cuerpo se ejecuta tan pronto como llamas a la función.
def gen_factory(func, seq):
"""Generator factory returning a generator."""
# do stuff ... immediately when factory gets called
print("build generator & return")
return (func(*args) for args in seq)
El segundo mymap
también es una fábrica, pero también es un generador en sí mismo, que proviene de un mymap
interior. Debido a que es un generador en sí mismo, la ejecución del cuerpo no se inicia hasta la primera invocación del siguiente (generador).
def gen_generator(func, seq):
"""Generator yielding from sub-generator inside."""
# do stuff ... first time when ''next'' gets called
print("build generator & yield")
yield from (func(*args) for args in seq)
Creo que el siguiente ejemplo lo hará más claro. Definimos paquetes de datos que se procesarán con funciones, agrupados en trabajos que pasamos a los generadores.
def add(a, b):
return a + b
def sqrt(a):
return a ** 0.5
data1 = [*zip(range(1, 5))] # [(1,), (2,), (3,), (4,)]
data2 = [(2, 1), (3, 1), (4, 1), (5, 1)]
job1 = (sqrt, data1)
job2 = (add, data2)
Ahora ejecutamos el siguiente código dentro de un shell interactivo como IPython para ver el comportamiento diferente. gen_factory
se imprime inmediatamente, mientras que gen_generator
solo lo hace después de que se gen_generator
next()
.
gen_fac = gen_factory(*job1)
# build generator & return <-- printed immediately
next(gen_fac) # start
# Out: 1.0
[*gen_fac] # deplete rest of generator
# Out: [1.4142135623730951, 1.7320508075688772, 2.0]
gen_gen = gen_generator(*job1)
next(gen_gen) # start
# build generator & yield <-- printed with first next()
# Out: 1.0
[*gen_gen] # deplete rest of generator
# Out: [1.4142135623730951, 1.7320508075688772, 2.0]
Para darle un ejemplo de caso de uso más razonable para un constructo como gen_generator
lo extenderemos un poco y crearemos una coroutina a partir de la asignación de rendimiento a las variables, por lo que podemos inyectar trabajos en el generador en ejecución con send()
.
Además, creamos una función auxiliar que ejecutará todas las tareas dentro de un trabajo y solicitará una nueva al finalizar.
def gen_coroutine():
"""Generator coroutine yielding from sub-generator inside."""
# do stuff... first time when ''next'' gets called
print("receive job, build generator & yield, loop")
while True:
try:
func, seq = yield "send me work ... or I quit with next next()"
except TypeError:
return "no job left"
else:
yield from (func(*args) for args in seq)
def do_job(gen, job):
"""Run all tasks in job."""
print(gen.send(job))
while True:
result = next(gen)
print(result)
if result == "send me work ... or I quit with next next()":
break
Ahora ejecutamos gen_coroutine
con nuestra función auxiliar do_job
y dos trabajos.
gen_co = gen_coroutine()
next(gen_co) # start
# receive job, build generator & yield, loop <-- printed with first next()
# Out:''send me work ... or I quit with next next()''
do_job(gen_co, job1) # prints out all results from job
# 1
# 1.4142135623730951
# 1.7320508075688772
# 2.0
# send me work... or I quit with next next()
do_job(gen_co, job2) # send another job into generator
# 3
# 4
# 5
# 6
# send me work... or I quit with next next()
next(gen_co)
# Traceback ...
# StopIteration: no job left
Para volver a su pregunta, ¿qué versión es el mejor enfoque en general? IMO algo así como gen_factory
solo tiene sentido si necesita hacer lo mismo para varios generadores que va a crear, o en casos su proceso de construcción para los generadores es lo suficientemente complicado como para justificar el uso de una fábrica en lugar de construir generadores individuales en su lugar con un generador comprensión.
Nota:
La descripción anterior para la función gen_generator
(segundo mymap
) dice " es un generador en sí mismo". Eso es un poco vago y técnicamente no es realmente correcto, pero facilita el razonamiento acerca de las diferencias de las funciones en esta configuración complicada donde gen_factory
también devuelve un generador, es decir, el creado por el generador en su interior.
De hecho, cualquier función (no solo las de esta pregunta con comprensión del generador en el interior) con un yield
dentro, tras la invocación, simplemente devuelve un objeto generador que se construye a partir del cuerpo de la función.
type(gen_coroutine) # function
gen_co = gen_coroutine(); type(gen_co) # generator
Entonces, toda la acción que observamos anteriormente para gen_generator
y gen_coroutine
tiene lugar dentro de estos objetos generadores, las funciones con yield
interior se han escupido antes.
Realmente depende de la situación. yield
se adapta principalmente a los casos en los que solo desea iterar sobre los valores devueltos y luego manipularlos. return
es adecuado principalmente cuando desea almacenar todos los valores que su función ha generado en la memoria en lugar de simplemente iterarlos una vez. Tenga en cuenta que solo puede iterar sobre un generador (lo que devuelve el rendimiento) una vez, hay algunos algoritmos para los cuales esto definitivamente no es adecuado.
Los generadores usan yield
, las funciones usan return
.
Los generadores se usan generalmente en los bucles para repetirse repetidamente sobre los valores proporcionados automáticamente por un generador , pero también pueden usarse en otro contexto, por ejemplo, en la función list () para crear una lista, nuevamente a partir de los valores proporcionados automáticamente por un generador .
Las funciones se llaman para proporcionar un valor de retorno , solo un valor para cada llamada.