¿Retorno o rendimiento de una función que llama a un generador?

30

Tengo un generador generatory también un método conveniente para ello generate_all.

def generator(some_list):
  for i in some_list:
    yield do_something(i)

def generate_all():
  some_list = get_the_list()
  return generator(some_list) # <-- Is this supposed to be return or yield?

Debería generate_all returno yield? Quiero que los usuarios de ambos métodos lo usen igual, es decir

for x in generate_all()

debe ser igual a

some_list = get_the_list()
for x in generate(some_list)
hyankov
fuente
2
Hay una razón para usar cualquiera. Para este ejemplo, el retorno es más eficiente
Mad Physicist el
1
Esto me recuerda una pregunta similar que planteé una vez: "rendimiento de iterable" vs "return iter (iterable)" . Si bien no se trata específicamente de generadores, es básicamente lo mismo que los generadores y los iteradores son bastante similares en Python. También la estrategia de comparar el código de byte propuesta por la respuesta puede ser útil aquí.
PeterE

Respuestas:

12

Los generadores tienen una evaluación perezosa, returno yieldse comportarán de manera diferente cuando depure su código o si se produce una excepción.

Con returncualquier excepción que ocurra en tu generatorno sabrás nada generate_all, eso es porque cuando generatorrealmente se ejecuta ya has dejado la generate_allfunción. Con yieldallí tendrá generate_allen el rastreo.

def generator(some_list):
    for i in some_list:
        raise Exception('exception happened :-)')
        yield i

def generate_all():
    some_list = [1,2,3]
    return generator(some_list)

for item in generate_all():
    ...
Exception                                 Traceback (most recent call last)
<ipython-input-3-b19085eab3e1> in <module>
      8     return generator(some_list)
      9 
---> 10 for item in generate_all():
     11     ...

<ipython-input-3-b19085eab3e1> in generator(some_list)
      1 def generator(some_list):
      2     for i in some_list:
----> 3         raise Exception('exception happened :-)')
      4         yield i
      5 

Exception: exception happened :-)

Y si está usando yield from:

def generate_all():
    some_list = [1,2,3]
    yield from generator(some_list)

for item in generate_all():
    ...
Exception                                 Traceback (most recent call last)
<ipython-input-4-be322887df35> in <module>
      8     yield from generator(some_list)
      9 
---> 10 for item in generate_all():
     11     ...

<ipython-input-4-be322887df35> in generate_all()
      6 def generate_all():
      7     some_list = [1,2,3]
----> 8     yield from generator(some_list)
      9 
     10 for item in generate_all():

<ipython-input-4-be322887df35> in generator(some_list)
      1 def generator(some_list):
      2     for i in some_list:
----> 3         raise Exception('exception happened :-)')
      4         yield i
      5 

Exception: exception happened :-)

Sin embargo, esto tiene un costo de rendimiento. La capa del generador adicional tiene algo de sobrecarga. Por returnlo tanto , generalmente será un poco más rápido que yield from ...(o for item in ...: yield item). En la mayoría de los casos, esto no importará mucho, porque todo lo que hagas en el generador generalmente domina el tiempo de ejecución para que la capa adicional no se note.

Sin embargo, yieldtiene algunas ventajas adicionales: no está restringido a un solo iterable, también puede generar fácilmente elementos adicionales:

def generator(some_list):
    for i in some_list:
        yield i

def generate_all():
    some_list = [1,2,3]
    yield 'start'
    yield from generator(some_list)
    yield 'end'

for item in generate_all():
    print(item)
start
1
2
3
end

En su caso, las operaciones son bastante simples y no sé si incluso es necesario crear múltiples funciones para esto, uno podría usar fácilmente la mapexpresión incorporada o una expresión generadora:

map(do_something, get_the_list())          # map
(do_something(i) for i in get_the_list())  # generator expression

Ambos deben ser idénticos (excepto algunas diferencias cuando ocurren excepciones) para usar. Y si necesitan un nombre más descriptivo, entonces aún podría envolverlos en una función.

Hay múltiples ayudantes que envuelven operaciones muy comunes en iterables integrados y se pueden encontrar más en el itertoolsmódulo incorporado . En casos tan simples, simplemente recurriría a estos y solo para casos no triviales escriba sus propios generadores.

Pero supongo que su código real es más complicado, por lo que puede no ser aplicable, pero pensé que no sería una respuesta completa sin mencionar alternativas.

MSeifert
fuente
17

Probablemente esté buscando Delegación de generador (PEP380)

Para iteradores simples, yield from iterablees esencialmente solo una forma abreviada defor item in iterable: yield item

def generator(iterable):
  for i in iterable:
    yield do_something(i)

def generate_all():
  yield from generator(get_the_list())

Es bastante conciso y también tiene una serie de otras ventajas, como ser capaz de encadenar iterables arbitrarios / diferentes.

ti7
fuente
Oh, te refieres al nombre de list? Es un mal ejemplo, no es un código real pegado en la pregunta, probablemente debería editarlo.
hyankov
Sí, no temas, soy bastante culpable de un código de ejemplo que ni siquiera se ejecutará en la primera pregunta ...
ti7
2
El primero también puede ser de una sola línea :). yield from map(do_something, iterable)o inclusoyield from (do_something(x) for x in iterable)
Físico loco el
2
"¡Es un código de ejemplo hasta el final!"
ti7
3
Solo necesita delegación si usted mismo está haciendo algo más que simplemente devolver el nuevo generador. Si solo devuelve el nuevo generador, no se necesita delegación. Por yield fromlo tanto, no tiene sentido a menos que su contenedor haga algo más generador-y.
ShadowRanger
14

return generator(list)hace lo que quieres Pero ten en cuenta que

yield from generator(list)

sería equivalente, pero con la oportunidad de producir más valores después de que generatorse agote. Por ejemplo:

def generator_all_and_then_some():
    list = get_the_list()
    yield from generator(list)
    yield "one last thing"
chepner
fuente
55
Creo que hay una sutil diferencia entre yield fromy returncuando el consumidor del generador es throwsuna excepción dentro de él, y con otras operaciones que están influenciadas por el seguimiento de la pila.
WorldSEnder
9

Las siguientes dos declaraciones parecerán ser funcionalmente equivalentes en este caso particular:

return generator(list)

y

yield from generator(list)

El último es aproximadamente lo mismo que

for i in generator(list):
    yield i

La returndeclaración devuelve el generador que está buscando. Una declaración yield fromo yieldconvierte toda su función en algo que devuelve un generador, que pasa por el que está buscando.

Desde el punto de vista del usuario, no hay diferencia. Sin embargo, internamente returnse puede decir que es más eficiente ya que no se envuelve generator(list)en un generador de paso superfluo. Si planea realizar algún procesamiento en los elementos del generador envuelto, utilice alguna forma, yieldpor supuesto.

Físico loco
fuente
4

Lo haría con returnél.

yielding * provocaría generate_all()evaluar a un generador en sí mismo, y llamar nexta ese generador externo devolvería el generador interno devuelto por la primera función, que no es lo que desearía.

* No incluído yield from

Carcigenicate
fuente