PEP 492 – Coroutinen mit async und await Syntax
- Autor:
- Yury Selivanov <yury at edgedb.com>
- Discussions-To:
- Python-Dev Liste
- Status:
- Final
- Typ:
- Standards Track
- Erstellt:
- 09-Apr-2015
- Python-Version:
- 3.5
- Post-History:
- 17-Apr-2015, 21-Apr-2015, 27-Apr-2015, 29-Apr-2015, 05-Mai-2015
Inhaltsverzeichnis
- Zusammenfassung
- API Design und Implementierungsrevisionen
- Begründung und Ziele
- Spezifikation
- Glossar
- Migrationsplan
- Designüberlegungen
- PEP 3152
- Coroutine-Generatoren
- Warum „async“- und „await“-Schlüsselwörter?
- Warum gibt „__aiter__“ kein Awaitable zurück?
- Bedeutung des „async“-Schlüsselworts
- Warum „async def“?
- Warum nicht „await for“ und „await with“?
- Warum „async def“ und nicht „def async“?
- Warum kein __future__-Import?
- Warum beginnen magische Methoden mit „a“?
- Warum nicht bestehende magische Namen wiederverwenden?
- Warum nicht bestehende „for“- und „with“-Anweisungen wiederverwenden?
- Comprehensions
- Async Lambda-Funktionen
- Performance
- Referenzimplementierung
- Akzeptanz
- Implementierung
- Referenzen
- Danksagungen
- Urheberrecht
Zusammenfassung
Das Wachstum des Internets und der allgemeinen Konnektivität hat den proportionalen Bedarf an reaktionsfähigem und skalierbarem Code ausgelöst. Dieser Vorschlag zielt darauf ab, diesem Bedarf gerecht zu werden, indem das Schreiben von explizit asynchronem, konkurrentem Python-Code einfacher und pythonischer wird.
Es wird vorgeschlagen, Coroutinen zu einem eigenständigen Konzept in Python zu machen und neue unterstützende Syntax einzuführen. Das ultimative Ziel ist es, ein gemeinsames, leicht zugängliches mentales Modell der asynchronen Programmierung in Python zu etablieren und es so nah wie möglich an die synchrone Programmierung heranzubringen.
Dieser PEP geht davon aus, dass die asynchronen Aufgaben von einer Ereignisschleife (Event Loop) geplant und koordiniert werden, ähnlich der des stdlib-Moduls asyncio.events.AbstractEventLoop. Während die PEP nicht an eine bestimmte Ereignisschleifen-Implementierung gebunden ist, ist sie nur für die Art von Coroutinen relevant, die yield als Signal an den Scheduler verwendet, um anzuzeigen, dass die Coroutine auf den Abschluss eines Ereignisses (wie z. B. E/A) wartet.
Wir glauben, dass die hier vorgeschlagenen Änderungen dazu beitragen werden, Python in einem schnell wachsenden Bereich der asynchronen Programmierung relevant und wettbewerbsfähig zu halten, da viele andere Sprachen ähnliche Funktionen übernommen haben oder planen, sie zu übernehmen: [2], [5], [6], [7], [8], [10].
API Design und Implementierungsrevisionen
- Das Feedback zur ersten Beta-Version von Python 3.5 führte zu einer Neugestaltung des Objektmodells, das diese PEP unterstützt, um native Coroutinen klarer von Generatoren zu trennen – anstatt eine neue Art von Generator zu sein, sind native Coroutinen nun ein völlig eigenständiger Typ (implementiert in [17]).
Diese Änderung wurde hauptsächlich aufgrund von Problemen implementiert, die bei dem Versuch auftraten, die Unterstützung für native Coroutinen in den Tornado-Webserver zu integrieren (berichtet in [18]).
- In CPython 3.5.2 wurde das Protokoll
__aiter__aktualisiert.Vor 3.5.2 sollte
__aiter__ein awaitable zurückgeben, das zu einem asynchronen Iterator aufgelöst wird. Ab 3.5.2 sollte__aiter__direkt asynchrone Iteratoren zurückgeben.Wenn das alte Protokoll in 3.5.2 verwendet wird, wird Python eine
PendingDeprecationWarningauslösen.In CPython 3.6 wird das alte
__aiter__-Protokoll weiterhin unterstützt, wobei eineDeprecationWarningausgelöst wird.In CPython 3.7 wird das alte
__aiter__-Protokoll nicht mehr unterstützt: Es wird einRuntimeErrorausgelöst, wenn__aiter__etwas anderes als einen asynchronen Iterator zurückgibt.
Begründung und Ziele
Das aktuelle Python unterstützt die Implementierung von Coroutinen über Generatoren (PEP 342), weiter verbessert durch die in PEP 380 eingeführte yield from-Syntax. Dieser Ansatz hat eine Reihe von Nachteilen
- Es ist einfach, Coroutinen mit regulären Generatoren zu verwechseln, da sie die gleiche Syntax teilen; dies gilt insbesondere für neue Entwickler.
- Ob eine Funktion eine Coroutine ist, wird durch das Vorhandensein von
yield- oderyield from-Anweisungen in ihrem Body bestimmt, was zu nicht offensichtlichen Fehlern führen kann, wenn solche Anweisungen während des Refactorings im Funktionskörper erscheinen oder verschwinden. - Die Unterstützung für asynchrone Aufrufe ist auf Ausdrücke beschränkt, bei denen
yieldsyntaktisch zulässig ist, was die Nützlichkeit von syntaktischen Merkmalen wiewith- undfor-Anweisungen einschränkt.
Dieser Vorschlag macht Coroutinen zu einem nativen Python-Sprachmerkmal und trennt sie klar von Generatoren. Dies beseitigt die Generator/Coroutine-Mehrdeutigkeit und ermöglicht die zuverlässige Definition von Coroutinen ohne Abhängigkeit von einer bestimmten Bibliothek. Dies ermöglicht auch Linter und IDEs, die statische Code-Analyse und das Refactoring zu verbessern.
Native Coroutinen und die zugehörigen neuen Syntaxmerkmale ermöglichen die Definition von Kontextmanager- und Iterationsprotokollen in asynchroner Form. Wie später in diesem Vorschlag gezeigt, ermöglicht die neue async with-Anweisung, dass Python-Programme beim Betreten und Verlassen eines Laufzeitkontexts asynchrone Aufrufe durchführen, und die neue async for-Anweisung macht es möglich, asynchrone Aufrufe in Iteratoren durchzuführen.
Spezifikation
Dieser Vorschlag führt neue Syntax und Semantik ein, um die Coroutinenunterstützung in Python zu verbessern.
Diese Spezifikation setzt Kenntnisse über die Implementierung von Coroutinen in Python voraus (PEP 342 und PEP 380). Die Motivation für die hier vorgeschlagenen Syntaxänderungen stammt aus dem asyncio-Framework (PEP 3156) und dem „Cofunctions“-Vorschlag (PEP 3152, der nun zugunsten dieser Spezifikation abgelehnt wurde).
Von nun an verwenden wir in diesem Dokument das Wort native Coroutine, um Funktionen zu bezeichnen, die mit der neuen Syntax deklariert werden. Generator-basierte Coroutine wird verwendet, wenn notwendig, um Coroutinen zu bezeichnen, die auf Generator-Syntax basieren. Coroutine wird in Kontexten verwendet, in denen beide Definitionen zutreffen.
Neue Coroutinen-Deklarationssyntax
Die folgende neue Syntax wird verwendet, um eine native Coroutine zu deklarieren
async def read_data(db):
pass
Schlüsseleigenschaften von Coroutinen
async def-Funktionen sind immer Coroutinen, auch wenn sie keineawait-Ausdrücke enthalten.- Es ist ein
SyntaxError, wennyield- oderyield from-Ausdrücke in einerasync-Funktion vorhanden sind. - Intern wurden zwei neue Code-Objekt-Flags eingeführt
CO_COROUTINEwird verwendet, um native Coroutinen (definiert mit neuer Syntax) zu markieren.CO_ITERABLE_COROUTINEwird verwendet, um generator-basierte Coroutinen mit native Coroutinen kompatibel zu machen (gesetzt durch die Funktion types.coroutine()).
- Reguläre Generatoren geben bei Aufruf ein Generator-Objekt zurück; ähnlich geben Coroutinen ein Coroutine-Objekt zurück.
StopIteration-Ausnahmen werden nicht aus Coroutinen weitergegeben und durch einRuntimeErrorersetzt. Für reguläre Generatoren erfordert ein solches Verhalten einen zukünftigen Import (siehe PEP 479).- Wenn eine native Coroutine gesammelt wird (garbage collected), wird eine
RuntimeWarningausgelöst, wenn sie nie awaited wurde (siehe auch Debugging Features). - Siehe auch die Sektion Coroutine objects.
types.coroutine()
Eine neue Funktion coroutine(fn) wird dem Modul types hinzugefügt. Sie ermöglicht die Interoperabilität zwischen bestehenden generator-basierten Coroutinen in asyncio und native Coroutinen, die durch diese PEP eingeführt werden.
@types.coroutine
def process_data(db):
data = yield from read_data(db)
...
Die Funktion wendet das Flag CO_ITERABLE_COROUTINE auf das Code-Objekt der Generator-Funktion an, wodurch ein Coroutine-Objekt zurückgegeben wird.
Wenn fn keine Generator-Funktion ist, wird sie umhüllt. Wenn sie einen Generator zurückgibt, wird dieser in ein awaitable Proxy-Objekt umhüllt (siehe unten die Definition von awaitable-Objekten).
Beachten Sie, dass das Flag CO_COROUTINE nicht von types.coroutine() angewendet wird, damit es möglich ist, native Coroutinen, die mit neuer Syntax definiert wurden, von generator-basierten Coroutinen zu unterscheiden.
Await-Ausdruck
Die folgende neue await-Ausdruck wird verwendet, um ein Ergebnis einer Coroutinen-Ausführung zu erhalten
async def read_data(db):
data = await db.fetch('SELECT ...')
...
await suspendiert, ähnlich wie yield from, die Ausführung der read_data-Coroutine, bis das awaitable db.fetch abgeschlossen ist und die Ergebnisdaten zurückgibt.
Es verwendet die yield from-Implementierung mit einem zusätzlichen Schritt zur Validierung seines Arguments. await akzeptiert nur ein awaitable, das eines der folgenden sein kann:
- Ein natives Coroutinen-Objekt, das von einer nativen Coroutinen-Funktion zurückgegeben wird.
- Ein generator-basiertes Coroutinen-Objekt, das von einer mit
types.coroutine()dekorierten Funktion zurückgegeben wird. - Ein Objekt mit einer Methode
__await__, die einen Iterator zurückgibt.Jede
yield from-Aufrufkette endet mit einemyield. Dies ist ein grundlegender Mechanismus, wie Futures implementiert werden. Da Coroutinen intern eine spezielle Art von Generatoren sind, wird jedesawaitdurch einyieldirgendwo in der Kette vonawait-Aufrufen suspendiert (siehe PEP 3156 für eine detaillierte Erklärung).Um dieses Verhalten für Coroutinen zu ermöglichen, wird eine neue magische Methode namens
__await__hinzugefügt. In asyncio muss beispielsweise, um Future-Objekte inawait-Anweisungen zu ermöglichen, nur die Zeile__await__ = __iter__zur Klasseasyncio.Futurehinzugefügt werden.Objekte mit der Methode
__await__werden im Rest dieser PEP als Future-ähnliche Objekte bezeichnet.Es ist ein
TypeError, wenn__await__etwas anderes als einen Iterator zurückgibt. - Objekte, die mit der CPython C API mit einer Funktion
tp_as_async.am_awaitdefiniert sind und einen Iterator zurückgeben (ähnlich der Methode__await__).
Es ist ein SyntaxError, await außerhalb einer async def-Funktion zu verwenden (genauso wie es ein SyntaxError ist, yield außerhalb einer def-Funktion zu verwenden).
Es ist ein TypeError, wenn etwas anderes als ein awaitable-Objekt an einen await-Ausdruck übergeben wird.
Aktualisierte Operator-Präzedenztabelle
Das Schlüsselwort await ist wie folgt definiert
power ::= await ["**" u_expr]
await ::= ["await"] primary
wobei „primary“ die am engsten gebundenen Operationen der Sprache darstellt. Seine Syntax ist
primary ::= atom | attributeref | subscription | slicing | call
Details finden Sie in der Python-Dokumentation [12] und im Abschnitt Grammar Updates dieses Vorschlags.
Der Hauptunterschied von await zu den Operatoren yield und yield from besteht darin, dass await-Ausdrücke in den meisten Fällen keine Klammern um sich herum benötigen.
Außerdem erlaubt yield from beliebige Ausdrücke als Argument, einschließlich Ausdrücken wie yield from a() + b(), die als yield from (a() + b()) geparst würden, was fast immer ein Fehler ist. Im Allgemeinen ist das Ergebnis einer arithmetischen Operation kein awaitable-Objekt. Um solche Fehler zu vermeiden, wurde beschlossen, die Präzedenz von await niedriger als die von [], () und ., aber höher als die von ** zu setzen.
| Operator | Description |
|---|---|
yield x, yield from x |
Yield-Ausdruck |
lambda |
Lambda-Ausdruck |
if – else |
Bedingter Ausdruck |
oder |
Boolean OR |
und |
Boolean AND |
not x |
Boolean NOT |
in, not in, is, is not, <, <=, >, >=, !=, == |
Vergleiche, einschließlich Mitgliedschafts- und Identitätstests |
| |
Bitweise OR |
^ |
Bitweise XOR |
& |
Bitweise AND |
<<, >> |
Shifts |
+, - |
Addition und Subtraktion |
*, @, /, //,
% |
Multiplikation, Matrixmultiplikation, Division, Rest |
+x, -x, ~x |
Positiv, negativ, bitweise NOT |
** |
Potenzierung |
await x |
Await-Ausdruck |
x[index], x[index:index], x(arguments...), x.attribute |
Subskription, Slicing, Aufruf, Attributreferenz |
(expressions...), [expressions...], {key: value...}, {expressions...} |
Bindungs- oder Tupelanzeige, Listenanzeige, Dictionary-Anzeige, Set-Anzeige |
Beispiele für „await“-Ausdrücke
Gültige Syntaxbeispiele
| Ausdruck | Wird geparst als |
|---|---|
if await fut: pass |
if (await fut): pass |
if await fut + 1: pass |
if (await fut) + 1: pass |
pair = await fut, 'spam' |
pair = (await fut), 'spam' |
with await fut, open(): pass |
with (await fut), open(): pass |
await foo()['spam'].baz()() |
await ( foo()['spam'].baz()() ) |
return await coro() |
return ( await coro() ) |
res = await coro() ** 2 |
res = (await coro()) ** 2 |
func(a1=await coro(), a2=0) |
func(a1=(await coro()), a2=0) |
await foo() + await bar() |
(await foo()) + (await bar()) |
-await foo() |
-(await foo()) |
Ungültige Syntaxbeispiele
| Ausdruck | Sollte geschrieben werden als |
|---|---|
await await coro() |
await (await coro()) |
await -coro() |
await (-coro()) |
Asynchrone Kontextmanager und „async with“
Ein asynchroner Kontextmanager ist ein Kontextmanager, der in seinen enter- und exit-Methoden die Ausführung pausieren kann.
Um dies zu ermöglichen, wird ein neues Protokoll für asynchrone Kontextmanager vorgeschlagen. Zwei neue magische Methoden werden hinzugefügt: __aenter__ und __aexit__. Beide müssen ein awaitable zurückgeben.
Ein Beispiel für einen asynchronen Kontextmanager
class AsyncContextManager:
async def __aenter__(self):
await log('entering context')
async def __aexit__(self, exc_type, exc, tb):
await log('exiting context')
Neue Syntax
Eine neue Anweisung für asynchrone Kontextmanager wird vorgeschlagen
async with EXPR as VAR:
BLOCK
die semantisch äquivalent ist zu
mgr = (EXPR)
aexit = type(mgr).__aexit__
aenter = type(mgr).__aenter__
VAR = await aenter(mgr)
try:
BLOCK
except:
if not await aexit(mgr, *sys.exc_info()):
raise
else:
await aexit(mgr, None, None, None)
Wie bei regulären with-Anweisungen ist es möglich, mehrere Kontextmanager in einer einzigen async with-Anweisung anzugeben.
Es ist ein Fehler, einen regulären Kontextmanager ohne __aenter__- und __aexit__-Methoden an async with zu übergeben. Es ist ein SyntaxError, async with außerhalb einer async def-Funktion zu verwenden.
Beispiel
Mit asynchronen Kontextmanagern ist es einfach, ordnungsgemäße Transaktionsmanager für Coroutinen zu implementieren
async def commit(session, data):
...
async with session.transaction():
...
await session.update(data)
...
Code, der Sperren benötigt, sieht ebenfalls leichter aus
async with lock:
...
anstatt
with (yield from lock):
...
Asynchrone Iteratoren und „async for“
Ein asynchrones Iterable kann in seiner iter-Implementierung asynchronen Code aufrufen, und ein asynchroner Iterator kann in seiner next-Methode asynchronen Code aufrufen. Um die asynchrone Iteration zu unterstützen
- Ein Objekt muss eine Methode
__aiter__implementieren (oder, wenn mit CPython C API definiert, den Slottp_as_async.am_aiter), die ein asynchrones Iterator-Objekt zurückgibt. - Ein asynchrones Iterator-Objekt muss eine Methode
__anext__implementieren (oder, wenn mit CPython C API definiert, den Slottp_as_async.am_anext), die ein awaitable zurückgibt. - Um die Iteration zu beenden, muss
__anext__eine AusnahmeStopAsyncIterationauslösen.
Ein Beispiel für ein asynchrones Iterable
class AsyncIterable:
def __aiter__(self):
return self
async def __anext__(self):
data = await self.fetch_data()
if data:
return data
else:
raise StopAsyncIteration
async def fetch_data(self):
...
Neue Syntax
Eine neue Anweisung zur Iteration über asynchrone Iteratoren wird vorgeschlagen
async for TARGET in ITER:
BLOCK
else:
BLOCK2
die semantisch äquivalent ist zu
iter = (ITER)
iter = type(iter).__aiter__(iter)
running = True
while running:
try:
TARGET = await type(iter).__anext__(iter)
except StopAsyncIteration:
running = False
else:
BLOCK
else:
BLOCK2
Es ist ein TypeError, wenn ein reguläres Iterable ohne __aiter__-Methode an async for übergeben wird. Es ist ein SyntaxError, async for außerhalb einer async def-Funktion zu verwenden.
Wie bei der regulären for-Anweisung hat async for eine optionale else-Klausel.
Beispiel 1
Mit dem asynchronen Iterationsprotokoll ist es möglich, Daten während der Iteration asynchron zu puffern
async for data in cursor:
...
Wobei cursor ein asynchroner Iterator ist, der nach jeder N-Iteration N Zeilen Daten aus einer Datenbank vorab abruft.
Der folgende Code illustriert das neue asynchrone Iterationsprotokoll
class Cursor:
def __init__(self):
self.buffer = collections.deque()
async def _prefetch(self):
...
def __aiter__(self):
return self
async def __anext__(self):
if not self.buffer:
self.buffer = await self._prefetch()
if not self.buffer:
raise StopAsyncIteration
return self.buffer.popleft()
dann kann die Cursor-Klasse wie folgt verwendet werden
async for row in Cursor():
print(row)
was dem folgenden Code entsprechen würde
i = Cursor().__aiter__()
while True:
try:
row = await i.__anext__()
except StopAsyncIteration:
break
else:
print(row)
Beispiel 2
Das Folgende ist eine Hilfsklasse, die ein reguläres Iterable in ein asynchrones umwandelt. Obwohl dies keine sehr nützliche Sache ist, veranschaulicht der Code die Beziehung zwischen regulären und asynchronen Iteratoren.
class AsyncIteratorWrapper:
def __init__(self, obj):
self._it = iter(obj)
def __aiter__(self):
return self
async def __anext__(self):
try:
value = next(self._it)
except StopIteration:
raise StopAsyncIteration
return value
async for letter in AsyncIteratorWrapper("abc"):
print(letter)
Warum StopAsyncIteration?
Coroutinen basieren intern immer noch auf Generatoren. So gab es vor PEP 479 keinen grundsätzlichen Unterschied zwischen
def g1():
yield from fut
return 'spam'
und
def g2():
yield from fut
raise StopIteration('spam')
Und da PEP 479 für Coroutinen akzeptiert und standardmäßig aktiviert ist, wird das folgende Beispiel seine StopIteration in ein RuntimeError eingehüllt haben
async def a1():
await fut
raise StopIteration('spam')
Der einzige Weg, dem externen Code mitzuteilen, dass die Iteration beendet ist, besteht darin, etwas anderes als StopIteration auszulösen. Daher wurde eine neue eingebaute Ausnahmeklasse StopAsyncIteration hinzugefügt.
Darüber hinaus werden mit der Semantik aus PEP 479 alle StopIteration-Ausnahmen, die in Coroutinen ausgelöst werden, in RuntimeError eingehüllt.
Coroutinen-Objekte
Unterschiede zu Generatoren
Dieser Abschnitt gilt nur für native Coroutinen mit dem Flag CO_COROUTINE, d. h. definiert mit der neuen Syntax async def.
Das Verhalten bestehender *generator-basierter Coroutinen* in asyncio bleibt unverändert.
Es wurden große Anstrengungen unternommen, um sicherzustellen, dass Coroutinen und Generatoren als unterschiedliche Konzepte behandelt werden.
- Native Coroutinen-Objekte implementieren keine Methoden
__iter__und__next__. Daher können sie nicht iteriert oder aniter(),list(),tuple()und andere eingebaute Funktionen übergeben werden. Sie können auch nicht in einerfor..in-Schleife verwendet werden.Ein Versuch,
__iter__oder__next__auf einem nativen Coroutinen-Objekt zu verwenden, führt zu einemTypeError. - Einfache Generatoren können keine nativen Coroutinen
yield fromen: Dies führt zu einemTypeError. - Generator-basierte Coroutinen (für asyncio-Code müssen sie mit
@asyncio.coroutine[1] dekoriert werden) können native Coroutinen-Objekteyield fromen. inspect.isgenerator()undinspect.isgeneratorfunction()gebenFalsefür native Coroutinen-Objekte und native Coroutinen-Funktionen zurück.
Methoden von Coroutinen-Objekten
Coroutinen basieren intern auf Generatoren, daher teilen sie sich die Implementierung. Ähnlich wie Generator-Objekte haben Coroutinen die Methoden throw(), send() und close(). StopIteration und GeneratorExit spielen die gleiche Rolle für Coroutinen (obwohl PEP 479 standardmäßig für Coroutinen aktiviert ist). Details finden Sie in PEP 342, PEP 380 und der Python-Dokumentation [11].
Die Methoden throw() und send() für Coroutinen werden verwendet, um Werte in Future-ähnliche Objekte zu pushen und Fehler in sie zu werfen.
Debugging-Funktionen
Ein häufiger Anfängerfehler ist das Vergessen, yield from auf Coroutinen anzuwenden
@asyncio.coroutine
def useful():
asyncio.sleep(1) # this will do nothing without 'yield from'
Zur Fehlersuche solcher Fehler gibt es einen speziellen Debug-Modus in asyncio, in dem der Dekorator @coroutine alle Funktionen mit einem speziellen Objekt mit einem Destruktor umschließt, der eine Warnung protokolliert. Immer wenn ein umwickelter Generator gesammelt wird, wird eine detaillierte Protokollnachricht mit Informationen darüber generiert, wo genau die Dekoratorfunktion definiert wurde, der Stack-Trace, wo sie gesammelt wurde, usw. Das Wrapper-Objekt stellt auch eine praktische Funktion __repr__ mit detaillierten Informationen über den Generator bereit.
Das einzige Problem ist, wie diese Debug-Fähigkeiten aktiviert werden. Da Debug-Einrichtungen im Produktionsmodus eine Nulloperation sein sollten, trifft der Dekorator @coroutine die Entscheidung, ob er umschließt oder nicht, basierend auf einer OS-Umgebungsvariable PYTHONASYNCIODEBUG. Auf diese Weise ist es möglich, asyncio-Programme mit den von asyncio bereitgestellten Funktionen instrumentiert auszuführen. EventLoop.set_debug, eine andere Debug-Einrichtung, hat keinen Einfluss auf das Verhalten des @coroutine-Dekorators.
Mit diesem Vorschlag ist die Coroutine ein natives Konzept, das sich von Generatoren unterscheidet. Zusätzlich zu einer RuntimeWarning, die für Coroutinen ausgelöst wird, die nie awaited wurden, wird vorgeschlagen, zwei neue Funktionen zum Modul sys hinzuzufügen: set_coroutine_wrapper und get_coroutine_wrapper. Dies dient dazu, erweiterte Debugging-Einrichtungen in asyncio und anderen Frameworks zu ermöglichen (wie z. B. die Anzeige, wo genau eine Coroutine erstellt wurde, und ein detaillierteres Stack-Trace, wo sie gesammelt wurde).
Neue Standardbibliotheksfunktionen
types.coroutine(gen). Siehe Abschnitt types.coroutine() für Details.inspect.iscoroutine(obj)gibtTruezurück, wennobjein natives Coroutinen-Objekt ist.inspect.iscoroutinefunction(obj)gibtTruezurück, wennobjeine native Coroutine-Funktion ist.inspect.isawaitable(obj)gibtTruezurück, wennobjein awaitable ist.inspect.getcoroutinestate(coro)gibt den aktuellen Zustand eines nativen Coroutinen-Objekts zurück (entsprichtinspect.getfgeneratorstate(gen)).inspect.getcoroutinelocals(coro)gibt die Zuordnung der lokalen Variablen eines nativen Coroutinen-Objekts zu ihren Werten zurück (entsprichtinspect.getgeneratorlocals(gen)).sys.set_coroutine_wrapper(wrapper)ermöglicht das Abfangen der Erstellung von nativen Coroutinen-Objekten.wrappermuss entweder ein aufrufbares Objekt sein, das ein Argument (ein Coroutine-Objekt) akzeptiert, oderNone.Nonesetzt den Wrapper zurück. Wenn die Funktion zweimal aufgerufen wird, ersetzt der neue Wrapper den vorherigen. Die Funktion ist threadspezifisch. Weitere Details finden Sie unter Debugging Features.sys.get_coroutine_wrapper()gibt das aktuelle Wrapper-Objekt zurück. GibtNonezurück, wenn kein Wrapper gesetzt wurde. Die Funktion ist threadspezifisch. Weitere Details finden Sie unter Debugging Features.
Neue abstrakte Basisklassen
Um eine bessere Integration mit bestehenden Frameworks (wie Tornado, siehe [13]) und Compilern (wie Cython, siehe [16]) zu ermöglichen, werden zwei neue Abstract Base Classes (ABC) hinzugefügt
collections.abc.AwaitableABC für *Future-ähnliche* Klassen, die die__await__Methode implementieren.collections.abc.CoroutineABC für *Coroutine*-Objekte, die diesend(value),throw(type, exc, tb),close()und__await__()Methoden implementieren.Beachten Sie, dass Generator-basierte Coroutinen mit dem Flag
CO_ITERABLE_COROUTINEdie Methode__await__nicht implementieren und daher keine Instanzen der ABCscollections.abc.Coroutineundcollections.abc.Awaitablesind.@types.coroutine def gencoro(): yield assert not isinstance(gencoro(), collections.abc.Coroutine) # however: assert inspect.isawaitable(gencoro())
Um das einfache Testen zu ermöglichen, ob Objekte asynchrone Iteration unterstützen, werden zwei weitere ABCs hinzugefügt
collections.abc.AsyncIterable– testet auf die__aiter__Methode.collections.abc.AsyncIterator– testet auf die__aiter__und__anext__Methoden.
Glossar
- Native Coroutine-Funktion
- Eine Coroutine-Funktion wird mit
async defdeklariert. Sie verwendetawaitundreturn value; siehe Neue Coroutine-Deklarationssyntax für Details. - Native Coroutine
- Zurückgegeben von einer nativen Coroutine-Funktion. Siehe Await-Ausdruck für Details.
- Generator-basierte Coroutine-Funktion
- Coroutinen basierend auf Generator-Syntax. Das häufigste Beispiel sind Funktionen, die mit
@asyncio.coroutinedekoriert sind. - Generator-basierte Coroutine
- Zurückgegeben von einer generator-basierten Coroutine-Funktion.
- Coroutine
- Entweder eine *native Coroutine* oder eine *generator-basierte Coroutine*.
- Coroutine-Objekt
- Entweder ein *natives Coroutine*-Objekt oder ein *generator-basiertes Coroutine*-Objekt.
- Future-ähnliches Objekt
- Ein Objekt mit einer
__await__Methode oder ein C-Objekt mit einer Funktiontp_as_async->am_await, das einen *Iterator* zurückgibt. Kann von einemawait-Ausdruck in einer Coroutine konsumiert werden. Eine Coroutine, die auf ein Future-ähnliches Objekt wartet, wird suspendiert, bis__await__des Future-ähnlichen Objekts abgeschlossen ist, und gibt das Ergebnis zurück. Siehe Await-Ausdruck für Details. - Awaitable
- Ein *Future-ähnliches* Objekt oder ein *Coroutine*-Objekt. Siehe Await-Ausdruck für Details.
- Asynchroner Kontextmanager
- Ein asynchroner Kontextmanager hat die Methoden
__aenter__und__aexit__und kann mitasync withverwendet werden. Siehe Asynchrone Kontextmanager und "async with" für Details. - Asynchroner Iterable
- Ein Objekt mit einer
__aiter__Methode, die ein *asynchroner Iterator*-Objekt zurückgeben muss. Kann mitasync forverwendet werden. Siehe Asynchrone Iteratoren und "async for" für Details. - Asynchroner Iterator
- Ein asynchroner Iterator hat eine
__anext__Methode. Siehe Asynchrone Iteratoren und "async for" für Details.
Migrationsplan
Um Rückwärtskompatibilitätsprobleme mit den Schlüsselwörtern async und await zu vermeiden, wurde entschieden, tokenizer.c so zu modifizieren, dass es
- die Kombination von
async defundNAMETokens erkennt; - während des Tokenisierens eines
async defBlocks, ersetzt es das'async'NAMEToken mitASYNCund das'await'NAMEToken mitAWAIT; - während des Tokenisierens eines
defBlocks, gibt es'async'und'await'NAMETokens unverändert aus.
Dieser Ansatz ermöglicht eine nahtlose Kombination neuer Syntaxmerkmale (alle nur in async Funktionen verfügbar) mit bestehendem Code.
Ein Beispiel für die gleichzeitige Verwendung von "async def" und einem "async"-Attribut in einem Code
class Spam:
async = 42
async def ham():
print(getattr(Spam, 'async'))
# The coroutine can be executed and will print '42'
Abwärtskompatibilität
Dieser Vorschlag bewahrt 100% Rückwärtskompatibilität.
asyncio
asyncio wurde angepasst und getestet, um mit Coroutinen und neuen Anweisungen zu arbeiten. Die Rückwärtskompatibilität ist zu 100% gewahrt, d.h. bestehender Code funktioniert wie gewohnt.
Die erforderlichen Änderungen sind hauptsächlich
- Modifizieren des
@asyncio.coroutineDekorators, um die neue Funktiontypes.coroutine()zu verwenden. - Hinzufügen der Zeile
__await__ = __iter__zur Klasseasyncio.Future. - Hinzufügen von
ensure_future()als Alias für die Funktionasync(). Kennzeichnen der Funktionasync()als veraltet.
asyncio Migrationsstrategie
Da *einfache Generatoren* keine *nativen Coroutine-Objekte* yield from können (siehe Abschnitt Unterschiede zu Generatoren für weitere Details), wird empfohlen, sicherzustellen, dass alle generator-basierten Coroutinen mit @asyncio.coroutine dekoriert werden, *bevor* mit der Verwendung der neuen Syntax begonnen wird.
async/await in der CPython-Codebasis
In CPython wird der Name await nicht verwendet.
async wird hauptsächlich von asyncio verwendet. Wir begegnen dem, indem wir die Funktion async() in ensure_future() umbenennen (siehe Abschnitt asyncio für Details).
Eine weitere Verwendung des Schlüsselworts async befindet sich in Lib/xml/dom/xmlbuilder.py, um ein Attribut async = False für die Klasse DocumentLS zu definieren. Es gibt keine Dokumentation oder Tests dafür, es wird nirgendwo sonst in CPython verwendet. Es wird durch einen Getter ersetzt, der eine DeprecationWarning auslöst und empfiehlt, stattdessen das Attribut async_ zu verwenden. Das Attribut 'async' ist nicht dokumentiert und wird in der CPython-Codebasis nicht verwendet.
Grammatik-Updates
Grammatikänderungen sind ziemlich minimal
decorated: decorators (classdef | funcdef | async_funcdef)
async_funcdef: ASYNC funcdef
compound_stmt: (if_stmt | while_stmt | for_stmt | try_stmt | with_stmt
| funcdef | classdef | decorated | async_stmt)
async_stmt: ASYNC (funcdef | with_stmt | for_stmt)
power: atom_expr ['**' factor]
atom_expr: [AWAIT] atom trailer*
Deprecation-Pläne
async und await werden in CPython 3.5 und 3.6 als "soft deprecated" (vorsichtig veraltet) markiert. In 3.7 werden sie zu vollwertigen Schlüsselwörtern. async und await bereits vor 3.7 zu vollwertigen Schlüsselwörtern zu machen, könnte die Portierung von Code nach Python 3 erschweren.
Designüberlegungen
PEP 3152
PEP 3152 von Gregory Ewing schlägt einen anderen Mechanismus für Coroutinen (genannt "Cofunctions") vor. Einige Kernpunkte:
- Ein neues Schlüsselwort
codefzur Deklaration einer *Cofunction*. Eine *Cofunction* ist immer ein Generator, auch wenn keinecocallAusdrücke darin vorkommen. Entsprichtasync defin diesem Vorschlag. - Ein neues Schlüsselwort
cocallzum Aufrufen einer *Cofunction*. Kann nur innerhalb einer *Cofunction* verwendet werden. Entsprichtawaitin diesem Vorschlag (mit einigen Unterschieden, siehe unten). - Es ist nicht möglich, eine *Cofunction* ohne das Schlüsselwort
cocallaufzurufen. cocallerfordert grammatikalisch Klammern danach.atom: cocall | <existing alternatives for atom> cocall: 'cocall' atom cotrailer* '(' [arglist] ')' cotrailer: '[' subscriptlist ']' | '.' NAME
cocall f(*args, **kwds)ist semantisch äquivalent zuyield from f.__cocall__(*args, **kwds).
Unterschiede zu diesem Vorschlag
- Es gibt keine Entsprechung zu
__cocall__in diesem PEP, die aufgerufen wird und deren Ergebnis anyield fromimcocallAusdruck übergeben wird. Das Schlüsselwortawaiterwartet ein *awaitable* Objekt, validiert den Typ und führt daraufyield fromaus. Obwohl die Methode__await__der Methode__cocall__ähnelt, wird sie nur zur Definition von *Future-ähnlichen* Objekten verwendet. awaitist in der Grammatik fast genauso definiert wieyield from(später wird durchgesetzt, dassawaitnur innerhalb vonasync defvorkommen kann). Es ist möglich, einfachawait futurezu schreiben, währendcocallimmer Klammern erfordert.- Damit asyncio mit PEP 3152 funktioniert, müsste der Dekorator
@asyncio.coroutinemodifiziert werden, um alle Funktionen in ein Objekt mit einer__cocall__Methode zu wrappen, oder__cocall__auf Generatoren zu implementieren. Um *Cofunctions* von bestehenden generator-basierten Coroutinen aufzurufen, müsste das Built-incostart(cofunc, *args, **kwargs)verwendet werden. - Da eine *Cofunction* nicht ohne das Schlüsselwort
cocallaufgerufen werden kann, verhindert dies automatisch den häufigen Fehler, dasyield fromfür generator-basierte Coroutinen zu vergessen. Dieser Vorschlag adressiert dieses Problem mit einem anderen Ansatz, siehe Debugging-Funktionen. - Ein Manko der Anforderung eines
cocallSchlüsselworts zum Aufrufen einer Coroutine ist, dass wenn wir uns entscheiden, Coroutine-Generatoren zu implementieren – Coroutinen mityieldoderasync yieldAusdrücken – wir keincocallSchlüsselwort bräuchten, um sie aufzurufen. So würden wir am Ende__cocall__und kein__call__für reguläre Coroutinen haben, und__call__und kein__cocall__für Coroutine-Generatoren. - Die grammatikalische Anforderung von Klammern führt auch zu einer ganzen Reihe neuer Probleme.
Der folgende Code
await fut await function_returning_future() await asyncio.gather(coro1(arg1, arg2), coro2(arg1, arg2))
würde so aussehen
cocall fut() # or cocall costart(fut) cocall (function_returning_future())() cocall asyncio.gather(costart(coro1, arg1, arg2), costart(coro2, arg1, arg2))
- Es gibt keine Entsprechungen zu
async forundasync within PEP 3152.
Coroutine-Generatoren
Mit dem Schlüsselwort async for ist das Konzept eines *Coroutine-Generators* wünschenswert – eine Coroutine mit yield und yield from Ausdrücken. Um Mehrdeutigkeiten mit regulären Generatoren zu vermeiden, würden wir wahrscheinlich ein async Schlüsselwort vor yield benötigen, und async yield from würde eine StopAsyncIteration Ausnahme auslösen.
Obwohl die Implementierung von Coroutine-Generatoren möglich ist, glauben wir, dass sie außerhalb des Geltungsbereichs dieses Vorschlags liegen. Dies ist ein fortgeschrittenes Konzept, das sorgfältig abgewogen und ausbalanciert werden muss, mit nicht-trivialen Änderungen an der Implementierung aktueller Generatorobjekte. Dies ist eine Angelegenheit für ein separates PEP.
Warum „async“- und „await“-Schlüsselwörter?
async/await ist kein neues Konzept in Programmiersprachen
- C# hat es schon lange [5];
- Vorschlag zur Aufnahme von async/await in ECMAScript 7 [2]; siehe auch Traceur-Projekt [9];
- Facebook’s Hack/HHVM [6];
- Google’s Dart-Sprache [7];
- Scala [8];
- Vorschlag zur Aufnahme von async/await in C++ [10];
- und viele andere weniger verbreitete Sprachen.
Dies ist ein enormer Vorteil, da einige Benutzer bereits Erfahrung mit async/await haben und es die Arbeit mit vielen Sprachen in einem Projekt erleichtert (z. B. Python mit ECMAScript 7).
Warum gibt „__aiter__“ kein Awaitable zurück?
PEP 492 wurde in CPython 3.5.0 mit __aiter__ als Methode akzeptiert, die ein Awaitable zurückgeben sollte, das zu einem asynchronen Iterator aufgelöst wird.
In 3.5.2 (da PEP 492 auf provisorischer Basis akzeptiert wurde) wurde das __aiter__ Protokoll aktualisiert, um direkt asynchrone Iteratoren zurückzugeben.
Die Motivation hinter dieser Änderung ist die Möglichkeit, asynchrone Generatoren in Python zu implementieren. Siehe [19] und [20] für weitere Details.
Bedeutung des „async“-Schlüsselworts
Obwohl es möglich wäre, einfach den await Ausdruck zu implementieren und alle Funktionen mit mindestens einem await als Coroutinen zu behandeln, erschwert dieser Ansatz das API-Design, die Code-Refaktorisierung und die langfristige Wartung.
Nehmen wir an, Python hätte nur das Schlüsselwort await.
def useful():
...
await log(...)
...
def important():
await useful()
Wenn die Funktion useful() refaktoriert und jemand alle await Ausdrücke daraus entfernt, würde sie zu einer regulären Python-Funktion werden, und aller Code, der von ihr abhängt, einschließlich important(), wäre fehlerhaft. Um dieses Problem zu mildern, müsste ein Dekorator ähnlich @asyncio.coroutine eingeführt werden.
Warum „async def“?
Für manche Leute mag die reine Syntax async name(): pass ansprechender sein als async def name(): pass. Es ist sicherlich einfacher zu tippen. Aber andererseits bricht es die Symmetrie zwischen async def, async with und async for, wo async ein Modifikator ist, der angibt, dass die Anweisung asynchron ist. Es ist auch konsistenter mit der bestehenden Grammatik.
Warum nicht „await for“ und „await with“?
async ist ein Adjektiv und daher eine bessere Wahl für ein Schlüsselwort als *Anweisungsqualifizierer*. await for/with würde implizieren, dass etwas auf den Abschluss einer for oder with Anweisung wartet.
Warum „async def“ und nicht „def async“?
async ist ein *Anweisungsqualifizierer*. Eine gute Analogie sind die Schlüsselwörter "static", "public", "unsafe" aus anderen Sprachen. "async for" ist eine asynchrone "for"-Anweisung, "async with" ist eine asynchrone "with"-Anweisung, "async def" ist eine asynchrone Funktion.
Das Vorhandensein von "async" nach dem Hauptanweisungsschlüsselwort könnte zu Verwirrung führen, z. B. "for async item in iterator" könnte als "für jedes asynchrone Element im Iterator" gelesen werden.
Das Vorhandensein des Schlüsselworts async vor def, with und for vereinfacht auch die Grammatik der Sprache. Und "async def" trennt Coroutinen visuell besser von regulären Funktionen.
Warum kein __future__-Import?
Der Abschnitt Übergangsplan erklärt, wie der Tokenizer modifiziert wird, um async und await *nur* in async def Blöcken als Schlüsselwörter zu behandeln. async def füllt somit die Rolle, die sonst eine Compiler-Deklaration auf Modulebene wie from __future__ import async_await ausfüllen würde.
Warum beginnen magische Methoden mit „a“?
Neue asynchrone magische Methoden __aiter__, __anext__, __aenter__ und __aexit__ beginnen alle mit dem gleichen Präfix "a". Ein alternativer Vorschlag wäre die Verwendung des Präfixes "async", so dass __anext__ zu __async_next__ wird. Um jedoch die neuen magischen Methoden an die bestehenden anzupassen, wie z. B. __radd__ und __iadd__, wurde beschlossen, die kürzere Version zu verwenden.
Warum nicht bestehende magische Namen wiederverwenden?
Eine alternative Idee zu neuen asynchronen Iteratoren und Kontextmanagern war die Wiederverwendung bestehender magischer Methoden durch Hinzufügen eines async Schlüsselworts zu ihren Deklarationen
class CM:
async def __enter__(self): # instead of __aenter__
...
Dieser Ansatz hat die folgenden Nachteile
- Es wäre nicht möglich, ein Objekt zu erstellen, das sowohl in
with- als auch inasync with-Anweisungen funktioniert; - Es würde die Rückwärtskompatibilität brechen, da nichts in Python <= 3.4 verbietet, Future-ähnliche Objekte von
__enter__und/oder__exit__zurückzugeben; - einer der Hauptpunkte dieses Vorschlags ist es, native Coroutinen so einfach und narrensicher wie möglich zu machen, daher die klare Trennung der Protokolle.
Warum nicht bestehende „for“- und „with“-Anweisungen wiederverwenden?
Die Vision hinter den bestehenden generator-basierten Coroutinen und diesem Vorschlag ist es, es Benutzern zu erleichtern, zu sehen, wo der Code möglicherweise suspendiert wird. Das Erkennen asynchroner Iteratoren und Kontextmanager durch bestehende "for"- und "with"-Anweisungen würde zwangsläufig implizite Suspend-Punkte schaffen und es schwieriger machen, den Code zu verstehen.
Comprehensions
Syntax für asynchrone Comprehensions könnte bereitgestellt werden, aber dieses Konstrukt liegt außerhalb des Geltungsbereichs dieses PEP.
Async Lambda-Funktionen
Syntax für asynchrone Lambda-Funktionen könnte bereitgestellt werden, aber dieses Konstrukt liegt außerhalb des Geltungsbereichs dieses PEP.
Performance
Gesamtauswirkung
Dieser Vorschlag hat keine beobachtbaren Leistungsauswirkungen. Hier ist eine Ausgabe des offiziellen Benchmark-Sets von Python [4]
python perf.py -r -b default ../cpython/python.exe ../cpython-aw/python.exe
[skipped]
Report on Darwin ysmac 14.3.0 Darwin Kernel Version 14.3.0:
Mon Mar 23 11:59:05 PDT 2015; root:xnu-2782.20.48~5/RELEASE_X86_64
x86_64 i386
Total CPU cores: 8
### etree_iterparse ###
Min: 0.365359 -> 0.349168: 1.05x faster
Avg: 0.396924 -> 0.379735: 1.05x faster
Significant (t=9.71)
Stddev: 0.01225 -> 0.01277: 1.0423x larger
The following not significant results are hidden, use -v to show them:
django_v2, 2to3, etree_generate, etree_parse, etree_process, fastpickle,
fastunpickle, json_dump_v2, json_load, nbody, regex_v8, tornado_http.
Tokenizer-Modifikationen
Es gibt keine beobachtbare Verlangsamung beim Parsen von Python-Dateien mit dem modifizierten Tokenizer: Das Parsen einer 12 MB großen Datei (Lib/test/test_binop.py 1000 Mal wiederholt) dauert genauso lange.
async/await
Das folgende Mikro-Benchmark wurde verwendet, um Leistungsunterschiede zwischen "async"-Funktionen und Generatoren zu ermitteln
import sys
import time
def binary(n):
if n <= 0:
return 1
l = yield from binary(n - 1)
r = yield from binary(n - 1)
return l + 1 + r
async def abinary(n):
if n <= 0:
return 1
l = await abinary(n - 1)
r = await abinary(n - 1)
return l + 1 + r
def timeit(func, depth, repeat):
t0 = time.time()
for _ in range(repeat):
o = func(depth)
try:
while True:
o.send(None)
except StopIteration:
pass
t1 = time.time()
print('{}({}) * {}: total {:.3f}s'.format(
func.__name__, depth, repeat, t1-t0))
Das Ergebnis ist, dass es keinen beobachtbaren Leistungsunterschied gibt
binary(19) * 30: total 53.321s
abinary(19) * 30: total 55.073s
binary(19) * 30: total 53.361s
abinary(19) * 30: total 51.360s
binary(19) * 30: total 49.438s
abinary(19) * 30: total 51.047s
Beachten Sie, dass eine Tiefe von 19 bedeutet, dass 1.048.575 Aufrufe getätigt werden.
Referenzimplementierung
Die Referenzimplementierung finden Sie hier: [3].
Liste der High-Level-Änderungen und neuen Protokolle
- Neue Syntax zur Definition von Coroutinen:
async defund neuesawaitSchlüsselwort. - Neue
__await__Methode für Future-ähnliche Objekte und neuertp_as_async.am_awaitSlot inPyTypeObject. - Neue Syntax für asynchrone Kontextmanager:
async with. Und zugehöriges Protokoll mit den Methoden__aenter__und__aexit__. - Neue Syntax für asynchrone Iteration:
async for. Und zugehöriges Protokoll mit__aiter__,__aexit__und der neuen eingebauten AusnahmeStopAsyncIteration. Neue Slotstp_as_async.am_aiterundtp_as_async.am_anextinPyTypeObject. - Neue AST-Knoten:
AsyncFunctionDef,AsyncFor,AsyncWith,Await. - Neue Funktionen:
sys.set_coroutine_wrapper(callback),sys.get_coroutine_wrapper(),types.coroutine(gen),inspect.iscoroutinefunction(func),inspect.iscoroutine(obj),inspect.isawaitable(obj),inspect.getcoroutinestate(coro)undinspect.getcoroutinelocals(coro). - Neue Bit-Flags
CO_COROUTINEundCO_ITERABLE_COROUTINEfür Code-Objekte. - Neue ABCs:
collections.abc.Awaitable,collections.abc.Coroutine,collections.abc.AsyncIterableundcollections.abc.AsyncIterator. - C API-Änderungen: neuer
PyCoro_Type(in Python alstypes.CoroutineTypeverfügbar) undPyCoroObject.PyCoro_CheckExact(*o)zum Testen, oboeine *native Coroutine* ist.
Obwohl die Liste der Änderungen und neuen Dinge nicht kurz ist, ist es wichtig zu verstehen, dass die meisten Benutzer diese Funktionen nicht direkt verwenden werden. Sie sind für die Verwendung in Frameworks und Bibliotheken vorgesehen, um Benutzern einfach zu bedienende und eindeutige APIs mit der Syntax async def, await, async for und async with zur Verfügung zu stellen.
Funktionierendes Beispiel
Alle in diesem PEP vorgeschlagenen Konzepte sind implementiert [3] und können getestet werden.
import asyncio
async def echo_server():
print('Serving on localhost:8000')
await asyncio.start_server(handle_connection,
'localhost', 8000)
async def handle_connection(reader, writer):
print('New connection...')
while True:
data = await reader.read(8192)
if not data:
break
print('Sending {:.10}... back'.format(repr(data)))
writer.write(data)
loop = asyncio.get_event_loop()
loop.run_until_complete(echo_server())
try:
loop.run_forever()
finally:
loop.close()
Akzeptanz
PEP 492 wurde von Guido am Dienstag, 5. Mai 2015 angenommen [14].
Implementierung
Die Implementierung wird in Issue 24017 verfolgt [15]. Sie wurde am 11. Mai 2015 committet.
Referenzen
Danksagungen
Ich danke Guido van Rossum, Victor Stinner, Elvis Pranskevichus, Andrew Svetlov, Łukasz Langa, Greg Ewing, Stephen J. Turnbull, Jim J. Jewett, Brett Cannon, Alyssa Coghlan, Steven D’Aprano, Paul Moore, Nathaniel Smith, Ethan Furman, Stefan Behnel, Paul Sokolovsky, Victor Petrovykh und vielen anderen für ihr Feedback, ihre Ideen, Bearbeitungen, Kritik, Code-Reviews und Diskussionen rund um dieses PEP.
Urheberrecht
Dieses Dokument wurde gemeinfrei erklärt.
Quelle: https://github.com/python/peps/blob/main/peps/pep-0492.rst
Zuletzt geändert: 2025-02-01 08:55:40 GMT