Ritorno o rendimento da una funzione che chiama un generatore?


30

Ho un generatore generatore anche un metodo conveniente per esso - 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?

Dovrebbe generate_all returno yield? Voglio che gli utenti di entrambi i metodi lo utilizzino allo stesso modo, vale a dire

for x in generate_all()

dovrebbe essere uguale a

some_list = get_the_list()
for x in generate(some_list)

2
C'è un motivo per usare entrambi. Per questo esempio, il ritorno è più efficiente
fisico pazzo il

1
Questo mi ricorda una domanda simile che ho posto una volta: "resa da iterabile" vs "ritorno iter (iterabile)" . Sebbene non sia specifico per i generatori, è sostanzialmente lo stesso dei generatori e gli iteratori sono abbastanza simili in Python. Anche la strategia di confronto del bytecode proposta dalla risposta può essere utile qui.
PeterE

Risposte:


12

I generatori lo stanno valutando in modo pigro returno yieldsi comporteranno in modo diverso durante il debug del codice o se viene generata un'eccezione.

Con returnqualsiasi eccezione che accade nel tuo generatornon ne saprai nulla generate_all, perché perché quando generatorviene realmente eseguito hai già lasciato la generate_allfunzione. Con yielddentro avrà generate_allnel traceback.

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 :-)

E se sta 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 :-)

Tuttavia, questo viene a scapito delle prestazioni. Il livello generatore aggiuntivo ha un certo sovraccarico. Quindi returnsarà generalmente un po 'più veloce di yield from ...(o for item in ...: yield item). Nella maggior parte dei casi questo non importa molto, perché qualsiasi cosa tu faccia nel generatore domina in genere il tempo di esecuzione in modo che il livello aggiuntivo non sia evidente.

Tuttavia yieldpresenta alcuni vantaggi aggiuntivi: non sei limitato a un singolo iterabile, puoi anche facilmente produrre elementi aggiuntivi:

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

Nel tuo caso le operazioni sono abbastanza semplici e non so se sia necessario creare funzioni multiple per questo, si potrebbe semplicemente usare l' mapespressione incorporata o un generatore invece:

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

Entrambi dovrebbero essere identici (ad eccezione di alcune differenze quando si verificano eccezioni) da utilizzare. E se hanno bisogno di un nome più descrittivo, allora potresti ancora avvolgerli in una funzione.

Esistono diversi helper che racchiudono operazioni molto comuni sugli iterabili incorporati e altri sono disponibili nel itertoolsmodulo integrato. In casi così semplici ricorrerei semplicemente a questi e solo per casi non banali scrivere i propri generatori.

Ma suppongo che il tuo vero codice sia più complicato, quindi potrebbe non essere applicabile, ma ho pensato che non sarebbe stata una risposta completa senza menzionare le alternative.


17

Probabilmente stai cercando la delegazione del generatore (PEP380)

Per semplici iteratori, yield from iterableè essenzialmente solo una forma abbreviata difor 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())

È piuttosto conciso e presenta anche una serie di altri vantaggi, come la possibilità di concatenare arbitrari / diversi iterabili!


Oh intendi la denominazione di list? È un cattivo esempio, non un vero codice incollato nella domanda, probabilmente dovrei modificarlo.
Hyankov,

Sì, non temere, sono abbastanza colpevole del codice di esempio che non verrà nemmeno eseguito a prima domanda ..
ti7

2
Anche il primo può essere un one-liner :). yield from map(do_something, iterable)o ancheyield from (do_something(x) for x in iterable)
Mad Physicist

2
"È un codice di esempio fino in fondo!"
7

3
Hai solo bisogno di una delega se tu stesso stai facendo qualcosa di diverso dalla semplice restituzione del nuovo generatore. Se si restituisce semplicemente il nuovo generatore, non è necessaria alcuna delega. Quindi yield fromè inutile a meno che il vostro involucro fa qualcosa di altro generatore-y.
ShadowRanger

14

return generator(list)fa quello che vuoi. Ma nota questo

yield from generator(list)

sarebbe equivalente, ma con l'opportunità di produrre più valori dopo che generatorè esaurito. Per esempio:

def generator_all_and_then_some():
    list = get_the_list()
    yield from generator(list)
    yield "one last thing"

5
Credo che ci sia una sottile differenza tra yield frome returnquando il consumatore del generatore fa throwsun'eccezione al suo interno - e con altre operazioni che sono influenzate dalla traccia dello stack.
WorldSEnder,

9

Le seguenti due affermazioni appariranno funzionalmente equivalenti in questo caso particolare:

return generator(list)

e

yield from generator(list)

Il successivo è approssimativamente lo stesso di

for i in generator(list):
    yield i

L' returnistruzione restituisce il generatore che stai cercando. Un'istruzione yield fromo yieldtrasforma l'intera funzione in qualcosa che restituisce un generatore, che passa attraverso quello che stai cercando.

Dal punto di vista dell'utente, non c'è differenza. Internamente, tuttavia, returnè probabilmente più efficiente poiché non si avvolge generator(list)in un superfluo generatore pass-through. Se hai intenzione di eseguire qualsiasi elaborazione sugli elementi del generatore di wrapping, usa yieldovviamente una forma .


4

Lo faresti return.

yielding * provocherebbe la generate_all()valutazione di un generatore stesso, e invocare nextquel generatore esterno restituirebbe il generatore interno restituito dalla prima funzione, che non è quello che vorresti.

* Non incluso yield from

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.