Was sind in der Praxis die Hauptanwendungen für die neue Syntax "Ausbeute von" in Python 3.3?

407

Es fällt mir schwer, mein Gehirn um PEP 380 zu wickeln .

  1. In welchen Situationen ist "Ertrag aus" sinnvoll?
  2. Was ist der klassische Anwendungsfall?
  3. Warum wird es mit Mikrofäden verglichen?

[Update]

Jetzt verstehe ich die Ursache meiner Schwierigkeiten. Ich habe Generatoren verwendet, aber nie wirklich Coroutinen (eingeführt von PEP-342 ). Trotz einiger Ähnlichkeiten sind Generatoren und Coroutinen grundsätzlich zwei verschiedene Konzepte. Das Verständnis der Coroutinen (nicht nur der Generatoren) ist der Schlüssel zum Verständnis der neuen Syntax.

IMHO Coroutinen sind die dunkelste Python-Funktion , die meisten Bücher lassen es nutzlos und uninteressant aussehen.

Vielen Dank für die tollen Antworten, aber besonderen Dank an agf und seinen Kommentar zu den Präsentationen von David Beazley . David rockt.

Paulo Scardine
quelle
7
Video von David Beazleys Präsentation von dabeaz.com/coroutines : youtube.com/watch?v=Z_OAlIhXziw
jcugat

Antworten:

571

Lassen Sie uns zuerst eines aus dem Weg räumen. Die Erklärung, yield from gdie gleichbedeutend ist, for v in g: yield v wird dem , worum yield fromes geht, nicht einmal gerecht . Seien wir ehrlich, wenn nur yield fromdie forSchleife erweitert wird, ist es nicht gerechtfertigt yield from, die Sprache zu erweitern und eine ganze Reihe neuer Funktionen in Python 2.x nicht zu implementieren.

Dadurch yield fromwird eine transparente bidirektionale Verbindung zwischen dem Anrufer und dem Subgenerator hergestellt :

  • Die Verbindung ist "transparent" in dem Sinne, dass sie auch alles korrekt weitergibt, nicht nur die generierten Elemente (z. B. Ausnahmen werden weitergegeben).

  • Die Verbindung ist "bidirektional" in dem Sinne, dass Daten sowohl von als auch zu einem Generator gesendet werden können.

( Wenn wir über TCP sprechen, yield from gkönnte dies bedeuten, dass "der Socket meines Clients vorübergehend getrennt und wieder mit diesem anderen Server-Socket verbunden wird". )

Übrigens, wenn Sie nicht sicher sind, was das Senden von Daten an einen Generator überhaupt bedeutet, müssen Sie zuerst alles löschen und über Coroutinen lesen - sie sind sehr nützlich (im Gegensatz zu Unterprogrammen ), aber in Python leider weniger bekannt. Dave Beazleys Curious Course on Coroutines ist ein ausgezeichneter Start. Lesen Sie die Folien 24-33 für eine schnelle Grundierung.

Lesen von Daten aus einem Generator mit Ertrag aus

def reader():
    """A generator that fakes a read from a file, socket, etc."""
    for i in range(4):
        yield '<< %s' % i

def reader_wrapper(g):
    # Manually iterate over data produced by reader
    for v in g:
        yield v

wrap = reader_wrapper(reader())
for i in wrap:
    print(i)

# Result
<< 0
<< 1
<< 2
<< 3

Anstatt manuell zu iterieren reader(), können wir es einfach tun yield from.

def reader_wrapper(g):
    yield from g

Das funktioniert und wir haben eine Codezeile entfernt. Und wahrscheinlich ist die Absicht etwas klarer (oder nicht). Aber nichts, was das Leben verändert.

Senden von Daten an einen Generator (Coroutine) unter Verwendung der Ausbeute aus - Teil 1

Jetzt machen wir etwas interessanteres. Erstellen wir eine Coroutine namens writer, die an sie gesendete Daten akzeptiert und in einen Socket, fd usw. schreibt.

def writer():
    """A coroutine that writes data *sent* to it to fd, socket, etc."""
    while True:
        w = (yield)
        print('>> ', w)

Die Frage ist nun, wie die Wrapper-Funktion das Senden von Daten an den Writer behandeln soll, damit alle Daten, die an den Wrapper gesendet werden, transparent an den Wrapper gesendet werden writer().

def writer_wrapper(coro):
    # TBD
    pass

w = writer()
wrap = writer_wrapper(w)
wrap.send(None)  # "prime" the coroutine
for i in range(4):
    wrap.send(i)

# Expected result
>>  0
>>  1
>>  2
>>  3

Der Wrapper muss die an ihn gesendeten Daten akzeptieren (offensichtlich) und sollte auch das verarbeiten, StopIterationwenn die for-Schleife erschöpft ist. Offensichtlich reicht for x in coro: yield xes nicht, nur etwas zu tun. Hier ist eine Version, die funktioniert.

def writer_wrapper(coro):
    coro.send(None)  # prime the coro
    while True:
        try:
            x = (yield)  # Capture the value that's sent
            coro.send(x)  # and pass it to the writer
        except StopIteration:
            pass

Oder wir könnten das tun.

def writer_wrapper(coro):
    yield from coro

Das spart 6 Codezeilen, macht es viel lesbarer und es funktioniert einfach. Magie!

Das Senden von Daten an einen Generator ergibt sich aus - Teil 2 - Ausnahmebehandlung

Machen wir es komplizierter. Was ist, wenn unser Autor Ausnahmen behandeln muss? Nehmen wir an, die writerGriffe a werden SpamExceptiongedruckt, ***wenn sie auf einen treffen.

class SpamException(Exception):
    pass

def writer():
    while True:
        try:
            w = (yield)
        except SpamException:
            print('***')
        else:
            print('>> ', w)

Was ist, wenn wir uns nicht ändern writer_wrapper? Funktioniert es? Lass es uns versuchen

# writer_wrapper same as above

w = writer()
wrap = writer_wrapper(w)
wrap.send(None)  # "prime" the coroutine
for i in [0, 1, 2, 'spam', 4]:
    if i == 'spam':
        wrap.throw(SpamException)
    else:
        wrap.send(i)

# Expected Result
>>  0
>>  1
>>  2
***
>>  4

# Actual Result
>>  0
>>  1
>>  2
Traceback (most recent call last):
  ... redacted ...
  File ... in writer_wrapper
    x = (yield)
__main__.SpamException

Ähm, es funktioniert nicht, weil x = (yield)nur die Ausnahme ausgelöst wird und alles zum Stillstand kommt. Lassen Sie es funktionieren, aber behandeln Sie Ausnahmen manuell und senden Sie sie oder werfen Sie sie in den Subgenerator ( writer)

def writer_wrapper(coro):
    """Works. Manually catches exceptions and throws them"""
    coro.send(None)  # prime the coro
    while True:
        try:
            try:
                x = (yield)
            except Exception as e:   # This catches the SpamException
                coro.throw(e)
            else:
                coro.send(x)
        except StopIteration:
            pass

Das funktioniert.

# Result
>>  0
>>  1
>>  2
***
>>  4

Aber das tut es auch!

def writer_wrapper(coro):
    yield from coro

Das yield fromtransparent behandelt das Senden der Werte oder das Werfen von Werten in den Subgenerator.

Dies deckt jedoch immer noch nicht alle Eckfälle ab. Was passiert, wenn der äußere Generator geschlossen ist? Was ist mit dem Fall, wenn der Untergenerator einen Wert zurückgibt (ja, in Python 3.3+ können Generatoren Werte zurückgeben), wie sollte der Rückgabewert weitergegeben werden? Das yield fromtransparente Handling aller Eckkoffer ist wirklich beeindruckend . yield fromfunktioniert einfach magisch und behandelt all diese Fälle.

Ich persönlich halte dies yield fromfür eine schlechte Keyword-Wahl, da dadurch die wechselseitige Natur nicht erkennbar wird. Es wurden andere Schlüsselwörter vorgeschlagen (wie delegateaber abgelehnt, da das Hinzufügen eines neuen Schlüsselworts zur Sprache viel schwieriger ist als das Kombinieren vorhandener.

Zusammengefasst ist es am besten zu denken , yield fromals transparent two way channelzwischen dem Anrufer und dem Teilgenerator.

Verweise:

  1. PEP 380 - Syntax zum Delegieren an einen Untergenerator (Ewing) [v3.3, 2009-02-13]
  2. PEP 342 - Coroutinen über erweiterte Generatoren (GvR, Eby) [v2.5, 2005-05-10]
Praveen Gollakota
quelle
3
@PraveenGollakota, im zweiten Teil Ihrer Frage, Senden von Daten an einen Generator (Coroutine) unter Verwendung der Ausbeute von - Teil 1 , was ist, wenn Sie mehr als Coroutinen haben, an die Sie den empfangenen Artikel weiterleiten können? Wie in einem Broadcaster- oder Abonnentenszenario, in dem Sie dem Wrapper in Ihrem Beispiel mehrere Coroutinen bereitstellen und Elemente an alle oder eine Teilmenge davon gesendet werden sollen?
Kevin Ghaboosi
3
@PraveenGollakota, Kudos für die tolle Antwort. Anhand der kleinen Beispiele kann ich die Dinge in Repl ausprobieren. Der Link zum Dave Beazley Kurs war ein Bonus!
BiGYaN
1
tun except StopIteration: passINSIDE die while True:Schleife ist nicht eine genaue Darstellung yield from coro- die nicht eine Endlosschleife ist und nach coroerschöpft ist (dh hebt StopIteration), writer_wrapperwird die nächste Anweisung ausführen. Nach der letzten Aussage wird es sich automatisch StopIterationwie jeder erschöpfte Generator erhöhen ...
Aprillion
1
... also wenn writerenthalten for _ in range(4)statt while True, dann würde es nach dem Drucken >> 3AUCH automatisch angehoben StopIterationund dies würde automatisch von behandelt yield fromund dann writer_wrapperautomatisch angehoben StopIterationund da wrap.send(i)es sich nicht innerhalb des tryBlocks befindet, würde es an diesem Punkt tatsächlich ausgelöst werden ( dh Traceback wird nur die Leitung mit melden wrap.send(i), nichts aus dem Inneren des Generators)
Aprillion
3
Beim Lesen von " fängt nicht einmal an, Gerechtigkeit zu üben " weiß ich, dass ich zur richtigen Antwort gekommen bin. Vielen Dank für die tolle Erklärung!
Hot.PxL
89

In welchen Situationen ist "Ertrag aus" sinnvoll?

Jede Situation, in der Sie eine Schleife wie diese haben:

for x in subgenerator:
  yield x

Wie der PEP beschreibt, ist dies ein eher naiver Versuch, den Subgenerator zu verwenden. Es fehlen einige Aspekte, insbesondere die ordnungsgemäße Handhabung der von PEP 342 eingeführten .throw()/ .send()/ .close()Mechanismen . Um dies richtig zu machen, ist ziemlich komplizierter Code notwendig.

Was ist der klassische Anwendungsfall?

Beachten Sie, dass Sie Informationen aus einer rekursiven Datenstruktur extrahieren möchten. Angenommen, wir möchten alle Blattknoten in einem Baum erhalten:

def traverse_tree(node):
  if not node.children:
    yield node
  for child in node.children:
    yield from traverse_tree(child)

Noch wichtiger ist die Tatsache, dass es bis yield fromdahin keine einfache Methode gab, den Generatorcode umzugestalten. Angenommen, Sie haben einen (sinnlosen) Generator wie diesen:

def get_list_values(lst):
  for item in lst:
    yield int(item)
  for item in lst:
    yield str(item)
  for item in lst:
    yield float(item)

Jetzt entscheiden Sie sich, diese Schleifen in separate Generatoren zu zerlegen. Ohne yield fromist dies hässlich, bis zu dem Punkt, an dem Sie zweimal überlegen werden, ob Sie es tatsächlich tun möchten. Mit yield fromist es eigentlich schön anzusehen:

def get_list_values(lst):
  for sub in [get_list_values_as_int, 
              get_list_values_as_str, 
              get_list_values_as_float]:
    yield from sub(lst)

Warum wird es mit Mikrofäden verglichen?

Ich denke, in diesem Abschnitt des PEP geht es darum, dass jeder Generator seinen eigenen isolierten Ausführungskontext hat. Zusammen mit der Tatsache, dass die Ausführung zwischen dem Generator-Iterator und dem Aufrufer unter Verwendung von yieldbzw. umgeschaltet __next__()wird, ähnelt dies Threads, bei denen das Betriebssystem den ausführenden Thread von Zeit zu Zeit zusammen mit dem Ausführungskontext (Stapel, Register, ...).

Der Effekt davon ist auch vergleichbar: Sowohl der Generator-Iterator als auch der Aufrufer schreiten gleichzeitig in ihrem Ausführungsstatus voran, ihre Ausführungen sind verschachtelt. Wenn der Generator beispielsweise eine Berechnung durchführt und der Anrufer die Ergebnisse druckt, werden die Ergebnisse angezeigt, sobald sie verfügbar sind. Dies ist eine Form der Parallelität.

Diese Analogie ist jedoch nicht spezifisch yield from- sie ist eher eine allgemeine Eigenschaft von Generatoren in Python.

Niklas B.
quelle
Das Refactoring von Generatoren ist heute schmerzhaft .
Josh Lee
1
Ich neige dazu, itertools häufig zum Refactoring von Generatoren zu verwenden (Dinge wie itertools.chain), es ist keine große Sache. Ich mag Ertrag aus, aber ich sehe immer noch nicht, wie revolutionär es ist. Es ist wahrscheinlich so, da Guido total verrückt danach ist, aber ich muss das große Ganze vermissen. Ich denke, es ist großartig für send (), da dies schwer zu überarbeiten ist, aber ich benutze das nicht oft.
E-
Ich nehme an, das get_list_values_as_xxxsind einfache Generatoren mit einer einzelnen Leitung for x in input_param: yield int(x)und die beiden anderen mit strundfloat
madtyn
@ NiklasB. re "Informationen aus einer rekursiven Datenstruktur extrahieren." Ich bin gerade dabei, Daten für Py zu sammeln. Könnten Sie einen Stich in dieses Q machen ?
Alancalvitti
33

Wo immer Sie einen Generator innerhalb eines Generators aufrufen, benötigen Sie eine "Pumpe", um yielddie Werte erneut zu ermitteln : for v in inner_generator: yield v. Wie der PEP hervorhebt, gibt es subtile Komplexitäten, die die meisten Menschen ignorieren. Nicht-lokale Flusskontrolle wie throw()ist ein Beispiel im PEP. Die neue Syntax yield from inner_generatorwird überall dort verwendet, wo Sie zuvor die explizite forSchleife geschrieben hätten . Es ist jedoch nicht nur syntaktischer Zucker: Es behandelt alle Eckfälle, die von der forSchleife ignoriert werden . "Zuckerhaltig" zu sein, ermutigt die Menschen, es zu verwenden und so das richtige Verhalten zu erreichen.

Diese Nachricht im Diskussionsthread spricht über diese Komplexität:

Mit den zusätzlichen Generatorfunktionen, die durch PEP 342 eingeführt wurden, ist dies nicht mehr der Fall: Wie in Gregs PEP beschrieben, unterstützt einfache Iteration send () und throw () nicht korrekt. Die Gymnastik, die zur Unterstützung von send () und throw () benötigt wird, ist eigentlich nicht so komplex, wenn man sie zerlegt, aber sie ist auch nicht trivial.

Ich kann nicht mit einem Vergleich mit Mikrofäden sprechen , außer zu beobachten, dass Generatoren eine Art Paralellismus sind. Sie können den angehaltenen Generator als einen Thread betrachten, der Werte über yieldeinen Consumer-Thread sendet . Die tatsächliche Implementierung ist möglicherweise nicht so (und die tatsächliche Implementierung ist offensichtlich für die Python-Entwickler von großem Interesse), betrifft jedoch nicht die Benutzer.

Die neue yield fromSyntax fügt der Sprache keine zusätzlichen Funktionen in Bezug auf das Threading hinzu, sondern erleichtert lediglich die korrekte Verwendung vorhandener Funktionen. Genauer gesagt erleichtert es einem unerfahrenen Verbraucher eines komplexen inneren Generators, der von einem Experten geschrieben wurde , diesen Generator zu durchlaufen, ohne seine komplexen Merkmale zu beeinträchtigen.

Ben Jackson
quelle
23

Ein kurzes Beispiel hilft Ihnen dabei, einen yield fromAnwendungsfall zu verstehen : Wert von einem anderen Generator abrufen

def flatten(sequence):
    """flatten a multi level list or something
    >>> list(flatten([1, [2], 3]))
    [1, 2, 3]
    >>> list(flatten([1, [2], [3, [4]]]))
    [1, 2, 3, 4]
    """
    for element in sequence:
        if hasattr(element, '__iter__'):
            yield from flatten(element)
        else:
            yield element

print(list(flatten([1, [2], [3, [4]]])))
ospider
quelle
2
Ich wollte nur vorschlagen, dass der Druck am Ende ohne die Konvertierung in eine Liste ein bisschen schöner aussehen würde -print(*flatten([1, [2], [3, [4]]]))
yoniLavi
6

yield from Grundsätzlich verkettet Iteratoren auf effiziente Weise:

# chain from itertools:
def chain(*iters):
    for it in iters:
        for item in it:
            yield item

# with the new keyword
def chain(*iters):
    for it in iters:
        yield from it

Wie Sie sehen können, wird eine reine Python-Schleife entfernt. Das ist so ziemlich alles, aber das Verketten von Iteratoren ist in Python ein weit verbreitetes Muster.

Threads sind im Grunde eine Funktion, mit der Sie an völlig zufälligen Punkten aus Funktionen herausspringen und in den Zustand einer anderen Funktion zurückkehren können. Der Thread-Supervisor führt dies sehr oft aus, sodass das Programm alle diese Funktionen gleichzeitig auszuführen scheint. Das Problem ist, dass die Punkte zufällig sind. Sie müssen also die Sperrung verwenden, um zu verhindern, dass der Supervisor die Funktion an einem problematischen Punkt stoppt.

Generatoren sind Threads in diesem Sinne ziemlich ähnlich: Mit ihnen können Sie bestimmte Punkte angeben (wann immer sie vorhanden sind yield), an denen Sie ein- und aussteigen können. Auf diese Weise werden Generatoren als Coroutinen bezeichnet.

Weitere Informationen finden Sie in diesen hervorragenden Tutorials zu Coroutinen in Python

Jochen Ritzel
quelle
10
Diese Antwort ist irreführend, da sie das hervorstechende Merkmal "Ausbeute von", wie oben erwähnt, beseitigt: send () und throw () support.
Justin W
2
@Justin W: Ich denke, was auch immer Sie zuvor gelesen haben, ist tatsächlich irreführend, weil Sie nicht verstanden haben, dass throw()/send()/close()es sich um yieldFunktionen handelt, die yield fromoffensichtlich ordnungsgemäß implementiert werden müssen, da sie den Code vereinfachen sollen. Solche Kleinigkeiten haben nichts mit Nutzung zu tun.
Jochen Ritzel
5
Bestreiten Sie Ben Jacksons Antwort oben? Ich lese Ihre Antwort so, dass es im Wesentlichen syntaktischer Zucker ist, der der von Ihnen bereitgestellten Codetransformation folgt. Die Antwort von Ben Jackson widerlegt diese Behauptung ausdrücklich.
Justin W
@JochenRitzel Sie müssen nie Ihre eigene chainFunktion schreiben, da diese itertools.chainbereits vorhanden ist. Verwenden Sie yield from itertools.chain(*iters).
Acumenus
4

In der angewandten Nutzung für den Asynchronous IO Koroutine , yield fromhat ein ähnliches Verhalten wie awaitin einer Koroutine Funktion . Beides wird verwendet, um die Ausführung von Coroutine auszusetzen.

Wenn für Asyncio keine ältere Python-Version (dh> 3.5) unterstützt werden muss, ist async def/ awaitdie empfohlene Syntax zum Definieren einer Coroutine. Somit yield fromwird in einer Coroutine nicht mehr benötigt.

Aber im Allgemeinen außerhalb der asyncio, yield from <sub-generator>hat noch eine andere Verwendung in Iterieren den Teilgenerators wie in der früheren Antwort erwähnt.

Yeo
quelle
1

Dieser Code definiert eine Funktion, fixed_sum_digitsdie einen Generator zurückgibt, der alle sechs Ziffern auflistet, so dass die Summe der Ziffern 20 beträgt.

def iter_fun(sum, deepness, myString, Total):
    if deepness == 0:
        if sum == Total:
            yield myString
    else:  
        for i in range(min(10, Total - sum + 1)):
            yield from iter_fun(sum + i,deepness - 1,myString + str(i),Total)

def fixed_sum_digits(digits, Tot):
    return iter_fun(0,digits,"",Tot) 

Versuchen Sie es ohne zu schreiben yield from. Wenn Sie einen effektiven Weg finden, lassen Sie es mich wissen.

Ich denke, dass in Fällen wie diesem: der Besuch von Bäumen yield fromden Code einfacher und sauberer macht.

jimifiki
quelle
0

Einfach ausgedrückt, yield frombietet Schwanzrekursion für Iteratorfunktionen.

DomQ
quelle