Zweck der Funktion "Senden" des Python-Generators?

163

Kann mir jemand ein Beispiel geben, warum die mit der Python-Generatorfunktion verknüpfte "Sende" -Funktion existiert? Ich verstehe die Ertragsfunktion vollständig. Die Sendefunktion ist für mich jedoch verwirrend. Die Dokumentation zu dieser Methode ist kompliziert:

generator.send(value)

Setzt die Ausführung fort und "sendet" einen Wert an die Generatorfunktion. Das Wertargument wird zum Ergebnis des aktuellen Ertragsausdrucks. Die send () -Methode gibt den nächsten vom Generator ausgegebenen Wert zurück oder löst StopIteration aus, wenn der Generator beendet wird, ohne einen anderen Wert zu liefern.

Was bedeutet das? Ich dachte, Wert war die Eingabe für die Funktion? Der Ausdruck "Die send () -Methode gibt den nächsten vom Generator ausgegebenen Wert zurück" scheint auch der genaue Zweck der Ertragsfunktion zu sein. Yield gibt den nächsten Wert zurück, den der Generator liefert ...

Kann mir jemand ein Beispiel für einen Generator geben, der send verwendet und etwas erreicht, was nicht möglich ist?

Tommy
quelle
3
Duplikat: stackoverflow.com/questions/12637768/…
Bas Swinckels
3
Ein weiteres Beispiel aus dem wirklichen Leben (Lesen von FTP) wurde hinzugefügt, wenn Rückrufe in Generatoren umgewandelt werden, die von innen verwendet werden
Jan Vlcinsky
2
Es ist erwähnenswert, dass "Wann send()aufgerufen wird, um den Generator zu starten, muss er Noneals Argument aufgerufen werden , da es keinen Ertragsausdruck gibt, der den Wert erhalten könnte.", Zitiert aus dem offiziellen Dokument und für das das Zitat in der Frage ist fehlt.
Rick

Antworten:

146

Es wird verwendet, um Werte an einen Generator zu senden, der gerade nachgegeben hat. Hier ist ein künstliches (nicht nützliches) Erklärungsbeispiel:

>>> def double_inputs():
...     while True:
...         x = yield
...         yield x * 2
...
>>> gen = double_inputs()
>>> next(gen)       # run up to the first yield
>>> gen.send(10)    # goes into 'x' variable
20
>>> next(gen)       # run up to the next yield
>>> gen.send(6)     # goes into 'x' again
12
>>> next(gen)       # run up to the next yield
>>> gen.send(94.3)  # goes into 'x' again
188.5999999999999

Sie können dies nicht nur mit tun yield.

Einer der besten Anwendungsfälle, die ich je gesehen habe, ist Twisted's @defer.inlineCallbacks. Im Wesentlichen können Sie damit eine Funktion wie die folgende schreiben:

@defer.inlineCallbacks
def doStuff():
    result = yield takesTwoSeconds()
    nextResult = yield takesTenSeconds(result * 10)
    defer.returnValue(nextResult / 10)

Was passiert , ist , dass die takesTwoSeconds()Erträge ein Deferred, der ein Wert ist viel versprechend Wert wird , wird später berechnet werden. Twisted kann die Berechnung in einem anderen Thread ausführen. Wenn die Berechnung abgeschlossen ist, wird sie an den verzögerten Wert übergeben, und der Wert wird dann an die doStuff()Funktion zurückgesendet. Somit doStuff()kann das mehr oder weniger wie eine normale prozedurale Funktion aussehen, außer dass es alle Arten von Berechnungen und Rückrufen usw. ausführen kann. Die Alternative vor dieser Funktionalität wäre, Folgendes zu tun:

def doStuff():
    returnDeferred = defer.Deferred()
    def gotNextResult(nextResult):
        returnDeferred.callback(nextResult / 10)
    def gotResult(result):
        takesTenSeconds(result * 10).addCallback(gotNextResult)
    takesTwoSeconds().addCallback(gotResult)
    return returnDeferred

Es ist viel komplizierter und unhandlicher.

Claudiu
quelle
2
Können Sie erklären, was der Zweck davon ist? Warum kann dies nicht mit double_inputs (Startnummer) und Yield neu erstellt werden?
Tommy
@ Tommy: Oh, weil die Werte, die du hast, nichts mit den vorherigen zu tun haben. Lassen Sie mich das Beispiel ändern
Claudiu
Warum sollten Sie dies dann über eine einfache Funktion verwenden, die ihre Eingabe verdoppelt?
Tommy
4
@ Tommy: Das würdest du nicht. Das erste Beispiel soll nur erklären, was es tut. Das zweite Beispiel ist für einen tatsächlich nützlichen Anwendungsfall.
Claudiu
1
@ Tommy: Ich würde sagen, wenn Sie wirklich wissen wollen, schauen Sie sich diese Präsentation an und arbeiten Sie alles durch. Eine kurze Antwort reicht nicht aus, denn dann sagst du einfach: "Aber kann ich das nicht einfach so machen?" usw.
Claudiu
96

Diese Funktion dient zum Schreiben von Coroutinen

def coroutine():
    for i in range(1, 10):
        print("From generator {}".format((yield i)))
c = coroutine()
c.send(None)
try:
    while True:
        print("From user {}".format(c.send(1)))
except StopIteration: pass

druckt

From generator 1
From user 2
From generator 1
From user 3
From generator 1
From user 4
...

Sehen Sie, wie die Steuerung hin und her geleitet wird? Das sind Coroutinen. Sie können für alle Arten von coolen Dingen wie Asynch IO und ähnlichem verwendet werden.

Stellen Sie sich das so vor, mit einem Generator und ohne Senden ist es eine Einbahnstraße

==========       yield      ========
Generator |   ------------> | User |
==========                  ========

Aber mit send wird es eine Einbahnstraße

==========       yield       ========
Generator |   ------------>  | User |
==========    <------------  ========
                  send

Die die Tür für den Benutzer das Anpassen der Generatoren Verhalten eröffnet im Fluge und den Generator für den Benutzer reagiert.

Daniel Gratzer
quelle
3
Eine Generatorfunktion kann jedoch Parameter annehmen. Wie geht "Senden" über das Senden eines Parameters an den Generator hinaus?
Tommy
13
@Tommy Weil Sie die Parameter eines laufenden Generators nicht ändern können. Sie geben ihm Parameter, es läuft, fertig. Mit send geben Sie ihm Parameter, es läuft ein bisschen, Sie senden ihm einen Wert und es macht etwas anderes, wiederholen Sie
Daniel Gratzer
2
@ Tommy Dies wird den Generator neu starten, was dazu führt, dass Sie viel Arbeit wiederholen müssen
Daniel Gratzer
5
Könnten Sie bitte den Zweck des Sendens eines None vor allem erläutern?
Shubham Aggarwal
2
@ShubhamAggarwal Der Generator wird gestartet. Es ist nur etwas, was getan werden muss. Es ist sinnvoll, wenn Sie darüber nachdenken, da send()der Generator beim ersten Aufruf das Schlüsselwort yieldnoch nicht erreicht hat.
Michael
50

Dies kann jemandem helfen. Hier ist ein Generator, der von der Sendefunktion nicht betroffen ist. Es nimmt den Parameter number bei der Instanziierung auf und bleibt von send unberührt:

>>> def double_number(number):
...     while True:
...         number *=2 
...         yield number
... 
>>> c = double_number(4)
>>> c.send(None)
8
>>> c.next()
16
>>> c.next()
32
>>> c.send(8)
64
>>> c.send(8)
128
>>> c.send(8)
256

Hier ist, wie Sie dieselbe Art von Funktion mit send ausführen würden, sodass Sie bei jeder Iteration den Wert von number ändern können:

def double_number(number):
    while True:
        number *= 2
        number = yield number

So sieht das aus, da das Senden eines neuen Werts für die Zahl das Ergebnis ändert:

>>> def double_number(number):
...     while True:
...         number *= 2
...         number = yield number
...
>>> c = double_number(4)
>>> 
>>> c.send(None)
8
>>> c.send(5) #10
10
>>> c.send(1500) #3000
3000
>>> c.send(3) #6
6

Sie können dies auch als solche in eine for-Schleife einfügen:

for x in range(10):
    n = c.send(n)
    print n

Weitere Hilfe finden Sie in diesem großartigen Tutorial .

radtek
quelle
12
Dieser Vergleich zwischen einer Funktion, die von send () nicht betroffen ist, und einer Funktion, die dies tut, hat wirklich geholfen. Vielen Dank!
Manas Bajaj
Wie kann dies ein anschauliches Beispiel für den Zweck von sein send? Ein Einfacher lambda x: x * 2macht dasselbe auf eine viel weniger verworrene Weise.
user209974
Verwendet es send? Geh und füge deine Antwort hinzu.
Radtek
17

Einige Anwendungsfälle für die Verwendung von Generator und send()

Generatoren mit send()erlauben:

  • Erinnern an den internen Zustand der Ausführung
    • In welchem ​​Schritt sind wir?
    • Wie ist der aktuelle Status unserer Daten?
  • Rückgabe einer Folge von Werten
  • Empfangssequenz von Eingaben

Hier sind einige Anwendungsfälle:

Beobachteter Versuch, einem Rezept zu folgen

Lassen Sie uns ein Rezept haben, das vordefinierte Eingaben in einer bestimmten Reihenfolge erwartet.

Wir können:

  • Erstellen Sie eine watched_attemptInstanz aus dem Rezept
  • lass es einige Eingaben bekommen
  • Bei jeder Eingabe werden Informationen darüber zurückgegeben, was sich derzeit im Pot befindet
  • Überprüfen Sie bei jeder Eingabe, ob die Eingabe die erwartete ist (und schlagen Sie fehl, wenn dies nicht der Fall ist).

    def recipe():
        pot = []
        action = yield pot
        assert action == ("add", "water")
        pot.append(action[1])
    
        action = yield pot
        assert action == ("add", "salt")
        pot.append(action[1])
    
        action = yield pot
        assert action == ("boil", "water")
    
        action = yield pot
        assert action == ("add", "pasta")
        pot.append(action[1])
    
        action = yield pot
        assert action == ("decant", "water")
        pot.remove("water")
    
        action = yield pot
        assert action == ("serve")
        pot = []
        yield pot

Um es zu verwenden, erstellen Sie zuerst die watched_attemptInstanz:

>>> watched_attempt = recipe()                                                                         
>>> watched_attempt.next()                                                                                     
[]                                                                                                     

Der Aufruf von .next()ist erforderlich, um die Ausführung des Generators zu starten.

Der zurückgegebene Wert zeigt an, dass unser Topf derzeit leer ist.

Führen Sie nun einige Aktionen aus, die den Erwartungen des Rezepts entsprechen:

>>> watched_attempt.send(("add", "water"))                                                                     
['water']                                                                                              
>>> watched_attempt.send(("add", "salt"))                                                                      
['water', 'salt']                                                                                      
>>> watched_attempt.send(("boil", "water"))                                                                    
['water', 'salt']                                                                                      
>>> watched_attempt.send(("add", "pasta"))                                                                     
['water', 'salt', 'pasta']                                                                             
>>> watched_attempt.send(("decant", "water"))                                                                  
['salt', 'pasta']                                                                                      
>>> watched_attempt.send(("serve"))                                                                            
[] 

Wie wir sehen, ist der Topf endlich leer.

Falls man das Rezept nicht befolgen würde, würde es scheitern (was das gewünschte Ergebnis eines beobachteten Kochversuchs sein könnte - nur zu lernen, dass wir nicht genug Aufmerksamkeit geschenkt haben, als wir Anweisungen erhielten.

>>> watched_attempt = running.recipe()                                                                         
>>> watched_attempt.next()                                                                                     
[]                                                                                                     
>>> watched_attempt.send(("add", "water"))                                                                     
['water']                                                                                              
>>> watched_attempt.send(("add", "pasta")) 

---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
<ipython-input-21-facdf014fe8e> in <module>()
----> 1 watched_attempt.send(("add", "pasta"))

/home/javl/sandbox/stack/send/running.py in recipe()
     29
     30     action = yield pot
---> 31     assert action == ("add", "salt")
     32     pot.append(action[1])
     33

AssertionError:

Beachte das:

  • Es gibt eine lineare Abfolge der erwarteten Schritte
  • Die Schritte können unterschiedlich sein (einige werden entfernt, andere werden dem Topf hinzugefügt).
  • Wir schaffen das alles mit einem Funktions- / Generator - ohne komplexe Klassen oder ähnliche Strukturen.

Laufende Summen

Wir können den Generator verwenden, um die laufende Summe der an ihn gesendeten Werte zu verfolgen.

Jedes Mal, wenn wir eine Zahl hinzufügen, wird die Anzahl der Eingaben und die Gesamtsumme zurückgegeben (gültig für den Moment, in dem die vorherige Eingabe gesendet wurde).

from collections import namedtuple

RunningTotal = namedtuple("RunningTotal", ["n", "total"])


def runningtotals(n=0, total=0):
    while True:
        delta = yield RunningTotal(n, total)
        if delta:
            n += 1
            total += delta


if __name__ == "__main__":
    nums = [9, 8, None, 3, 4, 2, 1]

    bookeeper = runningtotals()
    print bookeeper.next()
    for num in nums:
        print num, bookeeper.send(num)

Die Ausgabe würde folgendermaßen aussehen:

RunningTotal(n=0, total=0)
9 RunningTotal(n=1, total=9)
8 RunningTotal(n=2, total=17)
None RunningTotal(n=2, total=17)
3 RunningTotal(n=3, total=20)
4 RunningTotal(n=4, total=24)
2 RunningTotal(n=5, total=26)
1 RunningTotal(n=6, total=27)
Jan Vlcinsky
quelle
3
Ich führe Ihr Beispiel aus und in Python 3 scheint die Datei "watch_attempt.next ()" durch "next" ("watch_attempt") ersetzt zu werden.
thanos.a
15

Die send()Methode steuert den Wert links vom Ertragsausdruck.

Um zu verstehen, wie sich die Ausbeute unterscheidet und welchen Wert sie hat, aktualisieren wir zunächst schnell die Reihenfolge, in der der Python-Code ausgewertet wird.

Abschnitt 6.15 Auswertungsreihenfolge

Python wertet Ausdrücke von links nach rechts aus. Beachten Sie, dass bei der Auswertung einer Zuordnung die rechte Seite vor der linken Seite ausgewertet wird.

Ein Ausdruck auf a = bder rechten Seite wird also zuerst ausgewertet.

Wie das Folgende zeigt, wird a[p('left')] = p('right')die rechte Seite zuerst ausgewertet.

>>> def p(side):
...     print(side)
...     return 0
... 
>>> a[p('left')] = p('right')
right
left
>>> 
>>> 
>>> [p('left'), p('right')]
left
right
[0, 0]

Was macht Yield?, Yield, setzt die Ausführung der Funktion aus und kehrt zum Aufrufer zurück und setzt die Ausführung an der Stelle fort, an der sie vor dem Suspendieren aufgehört hat.

Wo genau wird die Ausführung ausgesetzt? Sie haben es vielleicht schon erraten ... die Ausführung wird zwischen der rechten und der linken Seite des Ertragsausdrucks angehalten. Daher wird new_val = yield old_valdie Ausführung am =Vorzeichen angehalten , und der Wert auf der rechten Seite (der vor dem Anhalten liegt und auch der an den Aufrufer zurückgegebene Wert ist) kann etwas anderes sein als der Wert auf der linken Seite (der Wert, der nach der Wiederaufnahme zugewiesen wird Ausführung).

yield ergibt 2 Werte, einen rechts und einen links.

Wie steuern Sie den Wert auf der linken Seite des Ertragsausdrucks? über die .send()Methode.

6.2.9. Ertragsausdrücke

Der Wert des Ertragsausdrucks nach der Wiederaufnahme hängt von der Methode ab, mit der die Ausführung fortgesetzt wurde. Wenn __next__()verwendet wird (normalerweise entweder über ein for oder das next()eingebaute), ist das Ergebnis None. Andernfalls send()ist das Ergebnis , wenn es verwendet wird, der an diese Methode übergebene Wert.

user2059857
quelle
13

Die sendMethode implementiert Coroutinen .

Wenn Sie Coroutines noch nicht kennengelernt haben, ist es schwierig, den Kopf herumzureißen, da sie die Art und Weise ändern, wie ein Programm abläuft. Sie können ein gutes Tutorial für weitere Details lesen .

Jochen Ritzel
quelle
6

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:

  1. Angenommen, es gist der Generator. g.sendschiebt einen Wert nach links durch das rechte Ende des Rohrs.
  2. Ausführung im Kontext von gPausen, damit der Körper der Generatorfunktion ausgeführt werden kann.
  3. Der g.senddurchgeschobene Wert wird nach links gezogen yieldund am linken Ende des Rohrs empfangen. In x = yield y, xwird dem gezogen Wert zugeordnet.
  4. Die Ausführung wird im Hauptteil der Generatorfunktion fortgesetzt, bis die nächste Zeile mit yielderreicht ist.
  5. 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 .
  6. 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.
  7. g.send setzt den Wert fort, zieht ihn ab und gibt ihn an den Benutzer zurück.
  8. 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:

  1. right_end.push(None) # the first half of g.send; sending None is what starts a generator
  2. right_end.pause()
  3. left_end.start()
  4. initial_value = left_end.pull()
  5. if initial_value is not None: raise TypeError("can't send non-None value to a just-started generator")
  6. left_end.do_stuff()
  7. left_end.push(y) # the first half of yield
  8. left_end.pause()
  9. right_end.resume()
  10. value1 = right_end.pull() # the second half of g.send
  11. right_end.do_stuff()
  12. right_end.push(value2) # the first half of g.send (again, but with a different value)
  13. right_end.pause()
  14. left_end.resume()
  15. x = left_end.pull() # the second half of yield
  16. 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:

  1. Wir haben die Implementierung in eine verschachtelte Funktion verschoben.
  2. 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.
  3. Innerhalb der verschachtelten Funktion, das erste , was wir tun , ist Prüfung , die left_end.pull()ist None, einen geschoben Wert im Prozess verbrauchen.
  4. Innerhalb der verschachtelten Funktion wurde die Anweisung x = yield ydurch zwei Zeilen ersetzt: left_end.push(y)und x = left_end.pull().
  5. 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.

KugelschreiberBen
quelle
"Das Argument 1 in g = f (1) wurde normal erfasst und y im Körper von f zugewiesen, aber die Weile True hat noch nicht begonnen." Warum nicht? Warum sollte Python nicht versuchen, diesen Code auszuführen, bis er z yield.
Josh
@Josh Der Cursor wird erst beim ersten Aufruf von vorgerückt send; Es dauert einen Aufruf von send(None), um den Cursor auf die erste yieldAnweisung zu bewegen , und erst dann sendsenden nachfolgende Aufrufe tatsächlich einen "echten" Wert an yield.
BallpointBen
Danke - Das ist interessant, damit der Dolmetscher weiß, dass die Funktion f irgendwann funktioniert yield, und also warten, bis er eine sendvom Anrufer erhält ? Mit einer normalen Funktion würde der Interpreter sofort mit der Ausführung beginnen f, oder? Schließlich gibt es in Python keinerlei AOT-Kompilierung. Sind Sie sicher, dass dies der Fall ist? (ohne zu hinterfragen, was Sie sagen, ich bin wirklich nur verwirrt über das, was Sie hier geschrieben haben). Wo kann ich mehr darüber lesen, wie Python weiß, dass es warten muss, bevor es den Rest der Funktion ausführt?
Josh
@Josh Ich habe dieses mentale Modell erstellt, indem ich beobachtet habe, wie verschiedene Spielzeuggeneratoren funktionieren, ohne die Interna von Python zu verstehen. Die Tatsache, dass die Initiale send(None)den entsprechenden Wert (z. B. 1) ergibt, ohne sie Nonean den Generator zu senden , legt jedoch nahe, dass der erste Aufruf von sendein Sonderfall ist. Es ist eine schwierige Oberfläche zum Entwerfen; Wenn Sie dem ersten sendeinen beliebigen Wert senden lassen , ist die Reihenfolge der ermittelten Werte und der gesendeten Werte im Vergleich zu der aktuellen um eins verschoben.
BallpointBen
Danke BallpointBen. Sehr interessant, ich habe hier eine Frage hinterlassen, um zu sehen, warum das so ist.
Josh
2

Diese verwirrten mich auch. Hier ist ein Beispiel, das ich gemacht habe, als ich versucht habe, einen Generator einzurichten, der Signale in abwechselnder Reihenfolge liefert und akzeptiert (Ausbeute, Akzeptieren, Ausbeute, Akzeptieren) ...

def echo_sound():

    thing_to_say = '<Sound of wind on cliffs>'
    while True:
        thing_to_say = (yield thing_to_say)
        thing_to_say = '...'.join([thing_to_say]+[thing_to_say[-6:]]*2)
        yield None  # This is the return value of send.

gen = echo_sound()

print 'You are lost in the wilderness, calling for help.'

print '------'
in_message = gen.next()
print 'You hear: "{}"'.format(in_message)
out_message = 'Hello!'
print 'You yell "{}"'.format(out_message)
gen.send(out_message)

print '------'
in_message = gen.next()
print 'You hear: "{}"'.format(in_message)
out_message = 'Is anybody out there?'
print 'You yell "{}"'.format(out_message)
gen.send(out_message)

print '------'
in_message = gen.next()
print 'You hear: "{}"'.format(in_message)
out_message = 'Help!'
print 'You yell "{}"'.format(out_message)
gen.send(out_message)

Die Ausgabe ist:

You are lost in the wilderness, calling for help.
------
You hear: "<Sound of wind on cliffs>"
You yell "Hello!"
------
You hear: "Hello!...Hello!...Hello!"
You yell "Is anybody out there?"
------
You hear: "Is anybody out there?...there?...there?"
You yell "Help!"
Peter
quelle