Rückkehr oder Ausbeute von einer Funktion, die einen Generator aufruft?

30

Ich habe einen Generator generatorund auch eine bequeme Methode dazu - 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?

Sollte generate_all returnoder yield? Ich möchte, dass die Benutzer beider Methoden es gleich verwenden, dh

for x in generate_all()

sollte gleich sein

some_list = get_the_list()
for x in generate(some_list)
Hyankov
quelle
2
Es gibt einen Grund, beides zu verwenden. In diesem Beispiel ist die Rückkehr effizienter
Mad Physicist
1
Dies erinnert mich an eine ähnliche Frage, die ich einmal gestellt habe: "Yield from iterable" vs. "return iter (iterable)" . Obwohl es sich nicht speziell um Generatoren handelt, ist es im Grunde dasselbe wie Generatoren und Iteratoren, die in Python ziemlich ähnlich sind. Auch die in der Antwort vorgeschlagene Strategie zum Vergleichen des Bytecodes kann hier von Nutzen sein.
PeterE

Antworten:

12

Generatoren werden faul ausgewertet returnoder yieldverhalten sich anders, wenn Sie Ihren Code debuggen oder wenn eine Ausnahme ausgelöst wird.

Mit returnAusnahme der Fälle, in denen Sie generatornichts wissen, liegt dies daran generate_all, dass generatorSie die generate_allFunktion bereits verlassen haben, wenn sie wirklich ausgeführt wird . Mit yielddrin wird es generate_allim Traceback sein.

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

Und wenn es verwendet 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 :-)

Dies geht jedoch zu Lasten der Leistung. Die zusätzliche Generatorschicht hat einen gewissen Overhead. Also returnwird in der Regel etwas schneller als yield from ...(oder for item in ...: yield item). In den meisten Fällen spielt dies keine große Rolle, da alles, was Sie im Generator tun, normalerweise die Laufzeit dominiert, sodass die zusätzliche Ebene nicht wahrgenommen wird.

Hat yieldjedoch einige zusätzliche Vorteile: Sie sind nicht auf eine einzige iterable beschränkt, sondern können auch problemlos zusätzliche Elemente liefern:

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

In Ihrem Fall sind die Operationen recht einfach und ich weiß nicht, ob es überhaupt notwendig ist, mehrere Funktionen dafür zu erstellen. Man könnte mapstattdessen einfach den eingebauten oder einen Generatorausdruck verwenden:

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

Beide sollten identisch sein (mit Ausnahme einiger Unterschiede, wenn Ausnahmen auftreten). Und wenn sie einen aussagekräftigeren Namen benötigen, können Sie sie dennoch in eine Funktion einschließen.

Es gibt mehrere Helfer, die sehr häufige Operationen für integrierte Iterables umschließen, und weitere finden Sie im integrierten itertoolsModul. In solch einfachen Fällen würde ich einfach auf diese zurückgreifen und nur für nicht triviale Fälle Ihre eigenen Generatoren schreiben.

Aber ich gehe davon aus, dass Ihr echter Code komplizierter ist, so dass er möglicherweise nicht anwendbar ist, aber ich dachte, es wäre keine vollständige Antwort, ohne Alternativen zu erwähnen.

MSeifert
quelle
17

Sie suchen wahrscheinlich nach Generator Delegation (PEP380)

Für einfache Iteratoren yield from iterableist im Wesentlichen nur eine verkürzte Form vonfor 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 ist ziemlich prägnant und hat auch eine Reihe anderer Vorteile, wie die Möglichkeit, beliebige / unterschiedliche Iterables zu verketten!

ti7
quelle
Oh du meinst die Benennung von list? Es ist ein schlechtes Beispiel, kein echter Code, der in die Frage eingefügt wurde. Ich sollte ihn wahrscheinlich bearbeiten.
Hyankov
Ja - keine Angst, ich bin ziemlich schuldig an Beispielcode, der nicht einmal auf den ersten
Blick ausgeführt wird
2
Erstens kann man auch ein Einzeiler sein :). yield from map(do_something, iterable)oder sogaryield from (do_something(x) for x in iterable)
Mad Physicist
2
"Es ist Beispielcode ganz unten!"
7.
3
Sie müssen nur delegiert werden, wenn Sie selbst etwas anderes tun als nur den neuen Generator zurückzugeben. Wenn Sie nur den neuen Generator zurückgeben, ist keine Delegierung erforderlich. Es yield fromist also sinnlos, es sei denn, Ihr Wrapper macht etwas anderes generatorisch.
ShadowRanger
14

return generator(list)macht was du willst. Aber beachte das

yield from generator(list)

wäre gleichwertig, aber mit der Möglichkeit, nach generatorErschöpfung mehr Werte zu erzielen . Zum Beispiel:

def generator_all_and_then_some():
    list = get_the_list()
    yield from generator(list)
    yield "one last thing"
chepner
quelle
5
Ich glaube, es gibt einen subtilen Unterschied zwischen yield fromund returnwann der Verbraucher des Generators throwseine Ausnahme darin macht - und mit anderen Operationen, die von der Stapelverfolgung beeinflusst werden.
WorldSEnder
9

Die folgenden zwei Aussagen scheinen in diesem speziellen Fall funktional äquivalent zu sein:

return generator(list)

und

yield from generator(list)

Letzteres ist ungefähr das gleiche wie

for i in generator(list):
    yield i

Die returnAnweisung gibt den gesuchten Generator zurück. Eine yield fromor- yieldAnweisung verwandelt Ihre gesamte Funktion in etwas, das einen Generator zurückgibt, der den gesuchten Generator durchläuft.

Aus Anwendersicht gibt es keinen Unterschied. Intern ist das jedoch returnwohl effizienter, da es nicht generator(list)in einen überflüssigen Durchgangsgenerator eingewickelt wird. Wenn Sie zu tun planen jede Verarbeitung auf die Elemente des umwickelten Generator, in irgendeiner Form verwenden yieldnatürlich.

Verrückter Physiker
quelle
4

Du würdest returnes tun .

yielding * würde dazu führen generate_all(), dass ein Generator selbst ausgewertet wird, und das Aufrufen nextdieses äußeren Generators würde den inneren Generator zurückgeben, der von der ersten Funktion zurückgegeben wird, was nicht das ist, was Sie wollen.

* Nicht mit inbegriffen yield from

Carcigenicate
quelle