PEP 380 – Syntax zum Delegieren an einen Untergenerator
- Autor:
- Gregory Ewing <greg.ewing at canterbury.ac.nz>
- Status:
- Final
- Typ:
- Standards Track
- Erstellt:
- 13. Feb 2009
- Python-Version:
- 3.3
- Post-History:
- Resolution:
- Python-Dev Nachricht
Zusammenfassung
Es wird eine Syntax vorgeschlagen, damit ein Generator einen Teil seiner Operationen an einen anderen Generator delegieren kann. Dies ermöglicht es, einen Codeabschnitt, der `yield` enthält, auszufedern und in einen anderen Generator zu verschieben. Darüber hinaus darf der Untergenerator mit einem Wert zurückkehren, und dieser Wert wird dem delegierenden Generator zur Verfügung gestellt.
Die neue Syntax eröffnet auch Möglichkeiten zur Optimierung, wenn ein Generator Werte, die von einem anderen erzeugt wurden, erneut liefert.
PEP-Akzeptanz
Guido hat die PEP am 26. Juni 2011 offiziell akzeptiert.
Motivation
Ein Python-Generator ist eine Form einer Koroutine, hat aber die Einschränkung, dass er nur an seinen unmittelbaren Aufrufer liefern kann. Das bedeutet, dass ein Codeabschnitt, der `yield` enthält, nicht auf die gleiche Weise wie anderer Code ausgefedert und in eine separate Funktion verschoben werden kann. Wenn man eine solche Ausfederung durchführt, wird die aufgerufene Funktion selbst zu einem Generator, und es ist notwendig, diesen zweiten Generator explizit zu durchlaufen und alle von ihm erzeugten Werte erneut zu liefern.
Wenn es nur um das Liefern von Werten geht, kann dies ohne größere Schwierigkeiten mit einer Schleife wie dieser durchgeführt werden
for v in g:
yield v
Wenn jedoch der Untergenerator ordnungsgemäß mit dem Aufrufer in Bezug auf Aufrufe von `send()`, `throw()` und `close()` interagieren soll, wird die Sache erheblich komplizierter. Wie später gezeigt wird, ist der benötigte Code sehr kompliziert und es ist schwierig, alle Randfälle korrekt zu behandeln.
Eine neue Syntax wird vorgeschlagen, um dieses Problem zu beheben. In den einfachsten Anwendungsfällen wird sie äquivalent zur obigen for-Schleife sein, aber sie wird auch die volle Bandbreite des Generatorverhaltens abdecken und es ermöglichen, Generatorcode einfach und unkompliziert zu refaktorisieren.
Vorschlag
Die folgende neue Ausdruckssyntax wird im Körper eines Generators erlaubt sein
yield from <expr>
wobei `
Darüber hinaus darf der Untergenerator, wenn der Iterator ein anderer Generator ist, eine `return`-Anweisung mit einem Wert ausführen, und dieser Wert wird zum Wert des `yield from`-Ausdrucks.
Die vollständige Semantik des `yield from`-Ausdrucks kann wie folgt anhand des Generatorprotokolls beschrieben werden
- Alle Werte, die der Iterator liefert, werden direkt an den Aufrufer weitergeleitet.
- Alle Werte, die mit `send()` an den delegierenden Generator gesendet werden, werden direkt an den Iterator weitergeleitet. Wenn der gesendete Wert None ist, wird die `__next__()`-Methode des Iterators aufgerufen. Wenn der gesendete Wert nicht None ist, wird die `send()`-Methode des Iterators aufgerufen. Wenn der Aufruf StopIteration auslöst, wird der delegierende Generator fortgesetzt. Jede andere Ausnahme wird an den delegierenden Generator weitergegeben.
- Ausnahmen außer GeneratorExit, die in den delegierenden Generator geworfen werden, werden an die `throw()`-Methode des Iterators weitergeleitet. Wenn der Aufruf StopIteration auslöst, wird der delegierende Generator fortgesetzt. Jede andere Ausnahme wird an den delegierenden Generator weitergegeben.
- Wenn eine GeneratorExit-Ausnahme in den delegierenden Generator geworfen wird oder die `close()`-Methode des delegierenden Generators aufgerufen wird, dann wird die `close()`-Methode des Iterators aufgerufen, falls er eine besitzt. Wenn dieser Aufruf eine Ausnahme ergibt, wird sie an den delegierenden Generator weitergegeben. Andernfalls wird GeneratorExit im delegierenden Generator ausgelöst.
- Der Wert des `yield from`-Ausdrucks ist das erste Argument der `StopIteration`-Ausnahme, die vom Iterator beim Beenden ausgelöst wird.
- `return expr` in einem Generator verursacht die Auslösung von `StopIteration(expr)` beim Beenden des Generators.
Erweiterungen für StopIteration
Zur Bequemlichkeit erhält die `StopIteration`-Ausnahme ein `value`-Attribut, das ihr erstes Argument speichert, oder None, wenn keine Argumente vorhanden sind.
Formale Semantik
In diesem Abschnitt wird die Python-3-Syntax verwendet.
- Die Anweisung
RESULT = yield from EXPR
ist semantisch äquivalent zu
_i = iter(EXPR) try: _y = next(_i) except StopIteration as _e: _r = _e.value else: while 1: try: _s = yield _y except GeneratorExit as _e: try: _m = _i.close except AttributeError: pass else: _m() raise _e except BaseException as _e: _x = sys.exc_info() try: _m = _i.throw except AttributeError: raise _e else: try: _y = _m(*_x) except StopIteration as _e: _r = _e.value break else: try: if _s is None: _y = next(_i) else: _y = _i.send(_s) except StopIteration as _e: _r = _e.value break RESULT = _r
- In einem Generator die Anweisung
return value
ist semantisch äquivalent zu
raise StopIteration(value)
es sei denn, die Ausnahme kann, wie derzeit, nicht von `except`-Klauseln innerhalb des zurückgebenden Generators abgefangen werden.
- Die StopIteration-Ausnahme verhält sich, als wäre sie so definiert
class StopIteration(Exception): def __init__(self, *args): if len(args) > 0: self.value = args[0] else: self.value = None Exception.__init__(self, *args)
Begründung
Das Refactoring-Prinzip
Die Begründung für die meisten der oben genannten Semantiken ergibt sich aus dem Wunsch, Generatorcode refaktorisieren zu können. Es sollte möglich sein, einen Codeabschnitt, der einen oder mehrere `yield`-Ausdrücke enthält, in eine separate Funktion zu verschieben (unter Verwendung der üblichen Techniken zur Behandlung von Referenzen auf Variablen im umgebenden Gültigkeitsbereich usw.) und die neue Funktion mit einem `yield from`-Ausdruck aufzurufen.
Das Verhalten des resultierenden zusammengesetzten Generators sollte, soweit vernünftigerweise praktikabel, in allen Situationen dasselbe sein wie das des ursprünglichen, nicht ausgefederten Generators, einschließlich Aufrufen von `__next__()`, `send()`, `throw()` und `close()`.
Die Semantik für Unteriteratoren, die keine Generatoren sind, wurde als vernünftige Verallgemeinerung des Generatorfalls gewählt.
Die vorgeschlagenen Semantiken haben die folgenden Einschränkungen in Bezug auf das Refactoring
- Ein Codeblock, der GeneratorExit abfängt, ohne es anschließend erneut auszulösen, kann nicht unter Beibehaltung des exakt gleichen Verhaltens ausgefedert werden.
- Ausgefedertes Code kann sich anders verhalten als nicht ausgefedertes Code, wenn eine StopIteration-Ausnahme in den delegierenden Generator geworfen wird.
Da die Anwendungsfälle dafür selten bis nicht vorhanden sind, wurde es nicht als lohnenswert erachtet, die zusätzliche Komplexität zu betreiben, die zu ihrer Unterstützung erforderlich wäre.
Finalisierung
Es gab einige Diskussionen darüber, ob die explizite Finalisierung des delegierenden Generators durch Aufrufen seiner `close()`-Methode, während er bei einem `yield from` angehalten ist, auch den Unteriterator finalisieren sollte. Ein Argument dagegen ist, dass dies zu einer vorzeitigen Finalisierung des Unteriterators führen würde, wenn Referenzen darauf an anderer Stelle existieren.
Die Berücksichtigung von Python-Implementierungen, die keine Referenzzählung verwenden, führte zu der Entscheidung, dass diese explizite Finalisierung durchgeführt werden sollte, damit das explizite Schließen eines ausgefederten Generators in allen Python-Implementierungen dieselbe Wirkung hat wie das Schließen eines nicht ausgefederten Generators.
Die Annahme ist, dass in den meisten Anwendungsfällen der Unteriterator nicht geteilt wird. Der seltene Fall eines geteilten Unteriterators kann durch eine Wrapper-Funktion, die `throw()`- und `close()`-Aufrufe blockiert, oder durch eine andere Methode als `yield from` zur Aufrufung des Unteriterators accomodiert werden.
Generatoren als Threads
Ein Motiv für Generatoren, die Werte zurückgeben können, liegt in der Verwendung von Generatoren zur Implementierung von leichtgewichtigen Threads. Wenn Generatoren auf diese Weise verwendet werden, ist es sinnvoll, die von einem leichtgewichtigen Thread durchgeführte Berechnung auf viele Funktionen zu verteilen. Man möchte einen Untergenerator wie eine gewöhnliche Funktion aufrufen, ihm Parameter übergeben und einen Rückgabewert erhalten können.
Mit der vorgeschlagenen Syntax kann eine Anweisung wie
y = f(x)
wobei f eine gewöhnliche Funktion ist, in einen Delegationsaufruf umgewandelt werden
y = yield from g(x)
wobei g ein Generator ist. Man kann über das Verhalten des resultierenden Codes nachdenken, indem man g als gewöhnliche Funktion betrachtet, die mit einer `yield`-Anweisung angehalten werden kann.
Wenn Generatoren auf diese Weise als Threads verwendet werden, ist man typischerweise nicht an den Werten interessiert, die in oder aus den Yields übergeben werden. Es gibt jedoch auch hierfür Anwendungsfälle, in denen der Thread als Erzeuger oder Verbraucher von Elementen betrachtet wird. Der `yield from`-Ausdruck ermöglicht es, die Logik des Threads auf beliebig viele Funktionen zu verteilen, wobei die Erzeugung oder der Verbrauch von Elementen in jeder Unterfunktion erfolgen kann und die Elemente automatisch von ihrer ultimativen Quelle oder zum ultimativen Ziel geleitet werden.
Bezüglich `throw()` und `close()` ist es vernünftig zu erwarten, dass, wenn eine Ausnahme von außen in den Thread geworfen wird, diese zuerst im innersten Generator, wo der Thread angehalten ist, ausgelöst und von dort nach außen weitergegeben wird; und dass, wenn der Thread von außen durch Aufrufen von `close()` beendet wird, die Kette der aktiven Generatoren von innen nach außen finalisiert wird.
Syntax
Die gewählte spezielle Syntax wurde so gewählt, dass sie ihre Bedeutung andeutet, keine neuen Schlüsselwörter einführt und sich klar von einem einfachen `yield` unterscheidet.
Optimierungen
Die Verwendung einer spezialisierten Syntax eröffnet Möglichkeiten zur Optimierung, wenn eine lange Kette von Generatoren besteht. Solche Ketten können beispielsweise beim rekursiven Durchlaufen einer Baumstruktur entstehen. Der Overhead der Weiterleitung von `__next__()`-Aufrufen und gelieferten Werten nach unten und oben in der Kette kann dazu führen, dass eine O(n)-Operation im schlimmsten Fall zu O(n**2) wird.
Eine mögliche Strategie ist, Generatorobjekten einen Slot hinzuzufügen, um einen delegierten Generator zu speichern. Wenn ein `__next__()`- oder `send()`-Aufruf auf dem Generator gemacht wird, wird zuerst dieser Slot überprüft, und wenn er nicht leer ist, wird stattdessen der von ihm referenzierte Generator fortgesetzt. Wenn er StopIteration auslöst, wird der Slot gelöscht und der Hauptgenerator fortgesetzt.
Dies würde den Delegationsaufwand auf eine Kette von C-Funktionsaufrufen ohne Python-Codeausführung reduzieren. Eine mögliche Verbesserung wäre, die gesamte Kette von Generatoren in einer Schleife zu durchlaufen und direkt den letzten zu reaktivieren, obwohl die Handhabung von StopIteration dann komplizierter ist.
Verwendung von StopIteration zur Rückgabe von Werten
Es gibt verschiedene Möglichkeiten, wie der Rückgabewert des Generators zurückgegeben werden könnte. Einige Alternativen sind die Speicherung als Attribut des Generator-Iterator-Objekts oder die Rückgabe als Wert des `close()`-Aufrufs an den Untergenerator. Der vorgeschlagene Mechanismus ist jedoch aus mehreren Gründen attraktiv
- Die Verwendung einer Verallgemeinerung der StopIteration-Ausnahme erleichtert es anderen Arten von Iteratoren, am Protokoll teilzunehmen, ohne ein zusätzliches Attribut oder eine close()-Methode entwickeln zu müssen.
- Sie vereinfacht die Implementierung, da der Punkt, an dem der Rückgabewert des Untergenerators verfügbar wird, derselbe Punkt ist, an dem die Ausnahme ausgelöst wird. Eine Verzögerung bis zu einem späteren Zeitpunkt würde erfordern, den Rückgabewert irgendwo zu speichern.
Abgelehnte Ideen
Einige Ideen wurden diskutiert, aber verworfen.
Vorschlag: Es sollte eine Möglichkeit geben, den anfänglichen Aufruf von __next__() zu verhindern oder ihn durch einen send()-Aufruf mit einem bestimmten Wert zu ersetzen, mit der Absicht, die Verwendung von Generatoren zu unterstützen, die so verpackt sind, dass der anfängliche __next__() automatisch durchgeführt wird.
Auflösung: Außerhalb des Umfangs des Vorschlags. Solche Generatoren sollten nicht mit `yield from` verwendet werden.
Vorschlag: Wenn das Schließen eines Unteriterators StopIteration mit einem Wert auslöst, geben Sie diesen Wert vom `close()`-Aufruf an den delegierenden Generator zurück.
Die Motivation für diese Funktion ist, dass das Ende eines Wertestroms, der an einen Generator gesendet wird, durch das Schließen des Generators signalisiert werden kann. Der Generator würde GeneratorExit abfangen, seine Berechnung abschließen und ein Ergebnis zurückgeben, das dann zum Rückgabewert des close()-Aufrufs wird.
Auflösung: Diese Verwendung von close() und GeneratorExit wäre mit ihrer aktuellen Rolle als Bail-out- und Cleanup-Mechanismus unvereinbar. Sie würde erfordern, dass beim Schließen eines delegierenden Generators nach dem Schließen des Untergenerators der delegierende Generator anstatt GeneratorExit erneut auszulösen, fortgesetzt wird. Dies ist jedoch nicht akzeptabel, da es nicht sicherstellt, dass der delegierende Generator im Falle eines Aufrufs von close() für Bereinigungszwecke ordnungsgemäß finalisiert wird.
Das Signalieren des Endes von Werten an einen Konsumenten wird besser durch andere Mittel adressiert, wie z. B. das Senden eines Sentinel-Werts oder das Auslösen einer zwischen Produzent und Konsument vereinbarten Ausnahme. Der Konsument kann dann den Sentinel oder die Ausnahme erkennen und darauf reagieren, indem er seine Berechnung abschließt und normal zurückkehrt. Ein solches Schema verhält sich im Beisein von Delegation korrekt.
Vorschlag: Wenn `close()` keinen Wert zurückgeben soll, dann lösen Sie eine Ausnahme aus, wenn StopIteration mit einem Nicht-None-Wert auftritt.
Auflösung: Kein klarer Grund dafür. Das Ignorieren eines Rückgabewerts wird in Python nirgendwo sonst als Fehler betrachtet.
Kritikpunkte
Unter diesem Vorschlag würde der Wert eines `yield from`-Ausdrucks auf eine sehr andere Weise als der eines gewöhnlichen `yield`-Ausdrucks abgeleitet werden. Dies legt nahe, dass eine andere Syntax, die das Wort `yield` nicht enthält, geeigneter wäre, aber bisher wurde keine akzeptable Alternative vorgeschlagen. Abgelehnte Alternativen sind `call`, `delegate` und `gcall`.
Es wurde vorgeschlagen, dass ein anderer Mechanismus als `return` im Untergenerator verwendet werden sollte, um den Wert festzulegen, der vom `yield from`-Ausdruck zurückgegeben wird. Dies würde jedoch das Ziel beeinträchtigen, den Untergenerator als pausierbare Funktion betrachten zu können, da er Werte nicht auf die gleiche Weise wie andere Funktionen zurückgeben könnte.
Die Verwendung einer Ausnahme zur Weitergabe des Rückgabewerts wurde als „Missbrauch von Ausnahmen“ kritisiert, ohne dass diese Behauptung konkret begründet wurde. In jedem Fall ist dies nur eine vorgeschlagene Implementierung; ein anderer Mechanismus könnte verwendet werden, ohne wesentliche Merkmale des Vorschlags zu verlieren.
Es wurde vorgeschlagen, dass eine andere Ausnahme, wie GeneratorReturn, anstelle von StopIteration verwendet werden sollte, um einen Wert zurückzugeben. Es wurden jedoch keine überzeugenden praktischen Gründe dafür vorgebracht, und die Hinzufügung eines `value`-Attributs zu StopIteration mildert Schwierigkeiten bei der Extraktion eines Rückgabewerts aus einer StopIteration-Ausnahme, die möglicherweise eines hat oder auch nicht. Außerdem würde die Verwendung einer anderen Ausnahme bedeuten, dass im Gegensatz zu gewöhnlichen Funktionen `return` ohne Wert in einem Generator nicht äquivalent zu `return None` wäre.
Alternative Vorschläge
Vorschläge ähnlicher Art wurden bereits früher gemacht, wobei einige die Syntax `yield *` anstelle von `yield from` verwendeten. Während `yield *` prägnanter ist, könnte man argumentieren, dass es einem gewöhnlichen `yield` zu ähnlich sieht und der Unterschied beim Lesen von Code übersehen werden könnte.
Soweit dem Autor bekannt ist, konzentrierten sich frühere Vorschläge nur auf das Liefern von Werten und litten daher unter der Kritik, dass die zweizeilige for-Schleife, die sie ersetzen, nicht mühsam genug zu schreiben ist, um eine neue Syntax zu rechtfertigen. Durch die Behandlung des vollständigen Generatorprotokolls bietet dieser Vorschlag erheblich mehr Vorteile.
Zusätzliches Material
Einige Beispiele für die Verwendung der vorgeschlagenen Syntax sind verfügbar, ebenso wie eine Prototypimplementierung, die auf der ersten oben skizzierten Optimierung basiert.
Eine für Python 3.3 aktualisierte Version der Implementierung ist vom Tracker Issue #11682 verfügbar.
Urheberrecht
Dieses Dokument wurde gemeinfrei erklärt.
Quelle: https://github.com/python/peps/blob/main/peps/pep-0380.rst
Zuletzt geändert: 2025-02-01 08:59:27 GMT