Das Wort "Ertrag" hat zwei Bedeutungen: etwas zu produzieren (z. B. Mais zu liefern) und anzuhalten, um jemanden / etwas anderes weiterlaufen zu lassen (z. B. Autos, die Fußgängern nachgeben). Beide Definitionen gelten für das yieldSchlüsselwort von Python . Das Besondere an Generatorfunktionen ist, dass im Gegensatz zu regulären Funktionen Werte an den Aufrufer "zurückgegeben" werden können, während eine Generatorfunktion lediglich angehalten und nicht beendet wird.
Es ist am einfachsten, sich einen Generator als ein Ende eines bidirektionalen Rohrs mit einem "linken" Ende und einem "rechten" Ende vorzustellen. Diese Pipe ist das Medium, über das Werte zwischen dem Generator selbst und dem Körper der Generatorfunktion gesendet werden. Jedes Ende der Pipe hat zwei Operationen: Diesepush sendet einen Wert und blockiert, bis das andere Ende der Pipe den Wert abruft und nichts zurückgibt. undpull, der blockiert, bis das andere Ende der Pipe einen Wert drückt, und den gedrückten Wert zurückgibt. Zur Laufzeit springt die Ausführung zwischen den Kontexten auf beiden Seiten der Pipe hin und her. Jede Seite wird ausgeführt, bis ein Wert an die andere Seite gesendet wird. An diesem Punkt wird sie angehalten, die andere Seite wird ausgeführt und wartet auf einen Wert in Rückkehr, an diesem Punkt hält die andere Seite an und es wird fortgesetzt. Mit anderen Worten, jedes Ende der Pipe verläuft von dem Moment, in dem es einen Wert empfängt, bis zu dem Moment, in dem es einen Wert sendet.
Das Rohr ist funktional symmetrisch, aber - vereinbarungs ich in dieser Antwort zu definieren - das linke Ende innerhalb der Generatorfunktion des Körpers nur verfügbar ist , und ist über das yieldSchlüsselwort, während das rechte Ende ist der Generator und ist über die Generatorfunktion send. Als singuläre Schnittstellen zu ihren jeweiligen Rohrenden yieldund mit senddoppelter Aufgabe: Sie drücken und ziehen jeweils Werte zu / von ihren Rohrenden, yielddrücken nach rechts und ziehen nach links, während sendsie das Gegenteil tun . Diese doppelte Pflicht ist der Kern der Verwirrung um die Semantik von Aussagen wie x = yield y. Brechen yieldund sendin zwei expliziten Push / Pull - Schritte wird ihre Semantik viel klarer machen:
- Angenommen, es
gist der Generator. g.sendschiebt einen Wert nach links durch das rechte Ende des Rohrs.
- Ausführung im Kontext von
gPausen, damit der Körper der Generatorfunktion ausgeführt werden kann.
- Der
g.senddurchgeschobene Wert wird nach links gezogen yieldund am linken Ende des Rohrs empfangen. In x = yield y, xwird dem gezogen Wert zugeordnet.
- Die Ausführung wird im Hauptteil der Generatorfunktion fortgesetzt, bis die nächste Zeile mit
yielderreicht ist.
yieldschiebt einen Wert nach rechts durch das linke Ende des Rohrs zurück nach g.send. In x = yield y, yist nach rechts durch das Rohr geschoben wird .
- Die Ausführung innerhalb des Körpers der Generatorfunktion wird angehalten, sodass der äußere Bereich dort fortgesetzt werden kann, wo er aufgehört hat.
g.send setzt den Wert fort, zieht ihn ab und gibt ihn an den Benutzer zurück.
- Wenn Sie
g.senddas nächste Mal aufgerufen werden, kehren Sie zu Schritt 1 zurück.
Während zyklisch, hat diese Prozedur einen Anfang: wann g.send(None)- was next(g)kurz ist - zum ersten Mal aufgerufen wird (es ist illegal, etwas anderes als Noneden ersten sendAufruf zu übergeben). Und es kann ein Ende haben: Wenn yieldim Körper der Generatorfunktion keine weiteren Aussagen mehr zu erreichen sind.
Sehen Sie, was die yieldAussage (oder genauer gesagt die Generatoren) so besonders macht? Im Gegensatz zum dürftigen returnSchlüsselwort kann yieldes Werte an seinen Aufrufer übergeben und Werte von seinem Aufrufer empfangen, ohne die Funktion zu beenden, in der es lebt! (Wenn Sie eine Funktion - oder einen Generator - beenden möchten, ist es natürlich praktisch, auch das returnSchlüsselwort zu haben .) Wenn eine yieldAnweisung gefunden wird, wird die Generatorfunktion lediglich angehalten und dann genau dort wieder aufgenommen, wo sie übrig geblieben ist aus, wenn ein anderer Wert gesendet wird. Und sendist nur die Schnittstelle für die Kommunikation mit dem Inneren einer Generatorfunktion von außerhalb.
Wenn wir wirklich diese Push / Pull / Rohr Analogie so weit wie möglich brechen wollen, haben wir am Ende mit dem folgenden Pseudo - Code auf das wirklich nach Hause fährt , dass, abgesehen von den Schritten 1-5, yieldund sendsind zwei Seiten der gleichen Münze Rohr:
right_end.push(None) # the first half of g.send; sending None is what starts a generator
right_end.pause()
left_end.start()
initial_value = left_end.pull()
if initial_value is not None: raise TypeError("can't send non-None value to a just-started generator")
left_end.do_stuff()
left_end.push(y) # the first half of yield
left_end.pause()
right_end.resume()
value1 = right_end.pull() # the second half of g.send
right_end.do_stuff()
right_end.push(value2) # the first half of g.send (again, but with a different value)
right_end.pause()
left_end.resume()
x = left_end.pull() # the second half of yield
goto 6
Der Schlüssel Transformation besteht darin , dass wir uns getrennt haben x = yield yund value1 = g.send(value2)jeweils in zwei Aussagen: left_end.push(y)und x = left_end.pull(); und value1 = right_end.pull()und right_end.push(value2). Es gibt zwei Sonderfälle des yieldSchlüsselworts: x = yieldund yield y. Dies sind syntaktische Zucker für x = yield Noneund _ = yield y # discarding value.
Spezifische Details bezüglich der genauen Reihenfolge, in der Werte durch das Rohr gesendet werden, finden Sie unten.
Was folgt, ist ein ziemlich langes konkretes Modell des Obigen. Erstens, es sollte zuerst angemerkt werden , dass für jeden Generator g, next(g)ist genau äquivalent zu g.send(None). In diesem Sinne können wir uns nur darauf konzentrieren, wie es sendfunktioniert, und nur über die Weiterentwicklung des Generators sprechen send.
Angenommen, wir haben
def f(y): # This is the "generator function" referenced above
while True:
x = yield y
y = x
g = f(1)
g.send(None) # yields 1
g.send(2) # yields 2
Nun die Definition von fungefähr Desugars für die folgende gewöhnliche (Nicht-Generator-) Funktion:
def f(y):
bidirectional_pipe = BidirectionalPipe()
left_end = bidirectional_pipe.left_end
right_end = bidirectional_pipe.right_end
def impl():
initial_value = left_end.pull()
if initial_value is not None:
raise TypeError(
"can't send non-None value to a just-started generator"
)
while True:
left_end.push(y)
x = left_end.pull()
y = x
def send(value):
right_end.push(value)
return right_end.pull()
right_end.send = send
# This isn't real Python; normally, returning exits the function. But
# pretend that it's possible to return a value from a function and then
# continue execution -- this is exactly the problem that generators were
# designed to solve!
return right_end
impl()
Bei dieser Transformation von ist Folgendes passiert f:
- Wir haben die Implementierung in eine verschachtelte Funktion verschoben.
- Wir haben eine bidirektionale Pipe erstellt,
left_endauf die von der verschachtelten Funktion right_endzugegriffen wird und auf die vom äußeren Bereich zurückgegeben und zugegriffen wird - das right_endist das, was wir als Generatorobjekt kennen.
- Innerhalb der verschachtelten Funktion, das erste , was wir tun , ist Prüfung , die
left_end.pull()ist None, einen geschoben Wert im Prozess verbrauchen.
- Innerhalb der verschachtelten Funktion wurde die Anweisung
x = yield ydurch zwei Zeilen ersetzt: left_end.push(y)und x = left_end.pull().
- Wir haben die
sendFunktion für definiert right_end, die das Gegenstück zu den beiden Zeilen ist, durch die wir die x = yield yAnweisung im vorherigen Schritt ersetzt haben.
In dieser Fantasiewelt, in der Funktionen nach der Rückkehr fortgesetzt werden können, gwerden sie zugewiesen right_endund dann impl()aufgerufen. Wenn wir also in unserem obigen Beispiel die Ausführung Zeile für Zeile verfolgen würden, würde ungefähr Folgendes passieren:
left_end = bidirectional_pipe.left_end
right_end = bidirectional_pipe.right_end
y = 1 # from g = f(1)
# None pushed by first half of g.send(None)
right_end.push(None)
# The above push blocks, so the outer scope halts and lets `f` run until
# *it* blocks
# Receive the pushed value, None
initial_value = left_end.pull()
if initial_value is not None: # ok, `g` sent None
raise TypeError(
"can't send non-None value to a just-started generator"
)
left_end.push(y)
# The above line blocks, so `f` pauses and g.send picks up where it left off
# y, aka 1, is pulled by right_end and returned by `g.send(None)`
right_end.pull()
# Rinse and repeat
# 2 pushed by first half of g.send(2)
right_end.push(2)
# Once again the above blocks, so g.send (the outer scope) halts and `f` resumes
# Receive the pushed value, 2
x = left_end.pull()
y = x # y == x == 2
left_end.push(y)
# The above line blocks, so `f` pauses and g.send(2) picks up where it left off
# y, aka 2, is pulled by right_end and returned to the outer scope
right_end.pull()
x = left_end.pull()
# blocks until the next call to g.send
Dies entspricht genau dem obigen 16-Stufen-Pseudocode.
Es gibt einige andere Details, wie z. B. wie sich Fehler ausbreiten und was passiert, wenn Sie das Ende des Generators erreichen (das Rohr ist geschlossen), aber dies sollte klar machen, wie der grundlegende Steuerungsfluss funktioniert, wenn er sendverwendet wird.
Schauen wir uns mit denselben Desugaring-Regeln zwei Sonderfälle an:
def f1(x):
while True:
x = yield x
def f2(): # No parameter
while True:
x = yield x
Zum größten Teil desugar sie auf die gleiche Weise wie f, die einzigen Unterschiede sind, wie die yieldAussagen transformiert werden:
def f1(x):
# ... set up pipe
def impl():
# ... check that initial sent value is None
while True:
left_end.push(x)
x = left_end.pull()
# ... set up right_end
def f2():
# ... set up pipe
def impl():
# ... check that initial sent value is None
while True:
left_end.push(x)
x = left_end.pull()
# ... set up right_end
Im ersten Fall wurde der Wert an übergeben f1 verschoben (nachgegeben), und dann werden alle gezogenen (gesendeten) Werte direkt zurückgeschoben (nachgegeben). Im zweiten Fall xhat (noch) keinen Wert, wenn es zum ersten Mal dazu kommt push, also wird ein UnboundLocalErrorerhöht.