720 Stimmen

Wofür wird die "yield from"-Syntax in Python 3.3 in der Praxis hauptsächlich verwendet?

Es fällt mir schwer, das zu begreifen. PEP 380 .

  1. Welche Situationen gibt es, in denen yield from nützlich ist?
  2. Was ist der klassische Anwendungsfall?
  3. Warum wird es mit Mikrofäden verglichen?

Bisher habe ich Generatoren verwendet, aber nie wirklich Coroutines (eingeführt von PEP-342 ). Trotz einiger Ähnlichkeiten handelt es sich bei Generatoren und Coroutines um zwei grundsätzlich unterschiedliche Konzepte. Das Verständnis von Coroutines (nicht nur von Generatoren) ist der Schlüssel zum Verständnis der neuen Syntax.

IMHO Coroutines sind die obskurste Python-Funktion In den meisten Büchern wird es als nutzlos und uninteressant dargestellt.


Vielen Dank für die tollen Antworten, aber besonderen Dank an agf und sein Kommentar mit einem Link zu David Beazley Vorträge .

1088voto

Praveen Gollakota Punkte 33644

Lassen Sie uns zunächst eine Sache aus dem Weg räumen. Die Erklärung, die yield from g ist gleichbedeutend mit for v in g: yield v wird dem nicht einmal ansatzweise gerecht zu was yield from dreht sich alles um. Denn, seien wir ehrlich, wenn alle yield from ist die Erweiterung der for Schleife, dann ist es nicht gerechtfertigt, die yield from der Sprache und verhindern, dass eine ganze Reihe neuer Funktionen in Python 2.x implementiert werden.

Was yield from ist es stellt eine transparente bidirektionale Verbindung zwischen dem Anrufer und dem Untergenerator her :

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

  • Die Verbindung ist "bidirektional" in dem Sinne, dass Daten sowohl gesendet als auch empfangen werden können. de y a einen Generator.

( Wenn wir über TCP sprechen würden, yield from g könnte bedeuten: "Trenne jetzt vorübergehend den Socket meines Clients und verbinde ihn wieder mit diesem anderen Server-Socket". )

Übrigens, wenn Sie nicht sicher sind, was Senden von Daten an einen Generator auch bedeutet, dass Sie alles stehen und liegen lassen müssen und über Koroutinen erstens - sie sind sehr nützlich (im Gegensatz zu Unterroutinen ), aber leider weniger bekannt in Python. Dave Beazleys kurioser Kurs über Coroutines ist ein ausgezeichneter Anfang. Folien 24-33 lesen für eine kurze Einweisung.

Lesen von Daten aus einem Generator unter Verwendung der Ausbeute von

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 eine Iteration über reader() können wir einfach yield from es.

def reader_wrapper(g):
    yield from g

Das funktioniert, und wir haben eine Zeile Code eingespart. Und wahrscheinlich ist die Absicht ein wenig klarer (oder auch nicht). Aber nichts Lebensveränderndes.

Senden von Daten an einen Generator (Coroutine) mit yield from - Teil 1

Jetzt wollen wir etwas Interessanteres machen. Lassen Sie uns eine Coroutine namens writer die an sie gesendete Daten annimmt und in ein Socket, fd usw. schreibt.

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

Nun stellt sich die Frage, wie die Wrapper-Funktion das Senden von Daten an den Writer handhaben soll, damit alle Daten, die an den Wrapper gesendet werden, transparent die an den 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 akzeptieren die Daten, die an ihn gesendet werden (natürlich) und sollte auch die StopIteration wenn die for-Schleife erschöpft ist. Offensichtlich macht man einfach for x in coro: yield x nicht ausreicht. 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 dies tun.

def writer_wrapper(coro):
    yield from coro

Das spart 6 Zeilen Code, macht ihn viel lesbarer und funktioniert einfach. Magie!

Senden von Daten an einen Generatorertrag von - Teil 2 - Ausnahmebehandlung

Machen wir es noch komplizierter. Was ist, wenn unser Autor Ausnahmen behandeln muss? Sagen wir, die writer behandelt ein SpamException und er druckt *** wenn es auf eine solche trifft.

class SpamException(Exception):
    pass

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

Was, wenn wir uns nicht ändern? writer_wrapper ? Funktioniert es? Versuchen wir es

# 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) löst einfach die Ausnahme aus und alles kommt zum Stillstand. Lassen Sie es funktionieren, aber behandeln Sie Ausnahmen manuell und senden Sie sie oder werfen Sie sie in den Untergenerator ( 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 hier tut es auch!

def writer_wrapper(coro):
    yield from coro

En yield from übernimmt transparent das Senden der Werte oder das Übergeben von Werten an den Untergenerator.

Damit sind aber immer noch nicht alle Eckfälle abgedeckt. Was passiert, wenn der äußere Generator geschlossen ist? Was ist mit dem Fall, dass 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 from die transparente Behandlung aller Eckfälle ist wirklich beeindruckend . yield from wie von Zauberhand funktioniert und alle diese Fälle behandelt.

Ich persönlich finde yield from ist ein schlecht gewähltes Schlüsselwort, denn es macht die Zwei-Wege Natur offensichtlich. Es wurden weitere Schlüsselwörter vorgeschlagen (wie delegate wurden jedoch abgelehnt, weil das Hinzufügen eines neuen Schlüsselworts zur Sprache viel schwieriger ist als die Kombination bestehender Schlüsselwörter.

Zusammenfassend lässt sich sagen, dass es am besten ist, an Folgendes zu denken yield from als transparent two way channel zwischen dem Aufrufer und dem Untergenerator.

Referenzen:

  1. PEP 380 - Syntax für die Delegierung an einen Untergenerator (Ewing) [v3.3, 2009-02-13]
  2. PEP 342 - Coroutinen über erweiterte Generatoren (GvR, Eby) [v2.5, 2005-05-10]

140voto

Niklas B. Punkte 88763

In welchen Situationen ist die "Ausbeute aus" nützlich?

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

for x in subgenerator:
  yield x

Wie im PEP beschrieben, ist dies ein ziemlich naiver Versuch, den Subgenerator zu benutzen, es fehlen mehrere Aspekte, insbesondere die korrekte Handhabung der .throw() / .send() / .close() Mechanismen eingeführt durch PEP 342 . Um dies richtig zu tun, ziemlich kompliziert Code erforderlich ist.

Was ist der klassische Anwendungsfall?

Nehmen wir an, Sie möchten Informationen aus einer rekursiven Datenstruktur extrahieren. Nehmen wir an, wir wollen alle Blattknoten in einem Baum ermitteln:

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 bis zum yield from Es gab keine einfache Methode zur Umgestaltung des Generatorcodes. 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)

Nun beschließen Sie, diese Schleifen in separate Generatoren aufzuteilen. Ohne yield from Das ist so hässlich, dass man sich zweimal überlegen wird, ob man das wirklich machen will. Mit yield from Es ist wirklich 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, was dieser Abschnitt im PEP ist, dass jeder Generator seinen eigenen isolierten Ausführungskontext hat. Zusammen mit der Tatsache, dass die Ausführung zwischen dem Generator-Iterator und dem Aufrufer mittels yield y __next__() Dies ist ähnlich wie bei Threads, bei denen das Betriebssystem von Zeit zu Zeit den ausführenden Thread zusammen mit dem Ausführungskontext (Stack, Register, ...) wechselt.

Die Wirkung ist ebenfalls vergleichbar: Sowohl der Generator-Iterator als auch der Aufrufer kommen in ihrem Ausführungszustand gleichzeitig voran, ihre Ausführungen sind verschachtelt. Wenn der Generator zum Beispiel eine Berechnung durchführt und der Aufrufer die Ergebnisse ausgibt, sehen Sie die Ergebnisse, sobald sie verfügbar sind. Dies ist eine Form der Gleichzeitigkeit.

Diese Analogie ist nicht spezifisch für yield from Es handelt sich dabei jedoch um eine allgemeine Eigenschaft von Generatoren in Python.

45voto

ospider Punkte 7323

Ein kurzes Beispiel soll Ihnen helfen, eines der folgenden Probleme zu verstehen yield from Anwendungsfall: Wert von einem anderen Generator erhalten

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

41voto

Ben Jackson Punkte 84305

Überall dort, wo Sie einen Generator aus einem Generator heraus aufrufen, benötigen Sie eine "Pumpe", um die yield die Werte: for v in inner_generator: yield v . Der PEP weist darauf hin, dass es hier subtile Komplexitäten gibt, die von den meisten Menschen ignoriert werden. Nicht-lokale Flusskontrolle wie throw() ist ein Beispiel aus dem PEP. Die neue Syntax yield from inner_generator wird an der Stelle verwendet, an der Sie die explizite Angabe for Schleife vor. Es ist jedoch nicht nur syntaktischer Zucker: Es behandelt alle Eckfälle, die von der for Schleife. Der "zuckrige" Charakter ermutigt die Menschen, ihn zu nutzen und so die richtigen Verhaltensweisen zu entwickeln.

Diese Nachricht im Diskussionsstrang spricht über diese komplexen Zusammenhänge:

Mit den zusätzlichen Generatorfunktionen, die durch PEP 342 eingeführt wurden, ist das kein nicht mehr der Fall: wie in Gregs PEP beschrieben, unterstützt die einfache Iteration nicht send() und throw() nicht korrekt unterstützt. Die Gymnastik, die zur Unterstützung von send() und throw() zu unterstützen, sind eigentlich nicht so komplex, wenn man sie aufschlüsselt, aber sie sind auch nicht trivial.

Ich kann nicht mit einem Vergleich mit Mikro-Threads, außer der Feststellung, dass Generatoren eine Art von Paralellismus sind. Sie können den angehaltenen Generator als einen Thread betrachten, der Werte über yield zu einem Verbraucher-Thread. Die tatsächliche Implementierung mag nicht so aussehen (und die tatsächliche Implementierung ist natürlich für die Python-Entwickler von großem Interesse), aber das betrifft die Benutzer nicht.

Die neue yield from Syntax fügt der Sprache keine zusätzlichen Fähigkeiten in Bezug auf das Threading hinzu, sondern erleichtert lediglich die korrekte Verwendung der vorhandenen Funktionen. Genauer gesagt macht sie es einfacher für einen Neuling Verbraucher eines komplexen inneren Generators, der von einem Experte diesen Generator zu durchlaufen, ohne seine komplexen Eigenschaften zu verletzen.

15voto

kat1330 Punkte 4766

yield gibt einen einzelnen Wert in die Sammlung ein.

yield from wird Sammlung in Sammlung umwandeln und sie flach machen.

Sehen Sie sich dieses Beispiel an:

def yieldOnly():
    yield "A"
    yield "B"
    yield "C"

def yieldFrom():
    for i in [1, 2, 3]:
        yield from yieldOnly()

test = yieldFrom()
for i in test:
print(i)

In der Konsole werden Sie sehen:

A
B
C
A
B
C
A
B
C

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X