Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python Enhancement Proposals

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

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

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

  2. 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 PendingDeprecationWarning auslösen.

    In CPython 3.6 wird das alte __aiter__-Protokoll weiterhin unterstützt, wobei eine DeprecationWarning ausgelöst wird.

    In CPython 3.7 wird das alte __aiter__-Protokoll nicht mehr unterstützt: Es wird ein RuntimeError ausgelöst, wenn __aiter__ etwas anderes als einen asynchronen Iterator zurückgibt.

    Weitere Details finden Sie unter [19] und [20].

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- oder yield 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 yield syntaktisch zulässig ist, was die Nützlichkeit von syntaktischen Merkmalen wie with- und for-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 keine await-Ausdrücke enthalten.
  • Es ist ein SyntaxError, wenn yield- oder yield from-Ausdrücke in einer async-Funktion vorhanden sind.
  • Intern wurden zwei neue Code-Objekt-Flags eingeführt
    • CO_COROUTINE wird verwendet, um native Coroutinen (definiert mit neuer Syntax) zu markieren.
    • CO_ITERABLE_COROUTINE wird 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 ein RuntimeError ersetzt. 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 RuntimeWarning ausgelö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 einem yield. Dies ist ein grundlegender Mechanismus, wie Futures implementiert werden. Da Coroutinen intern eine spezielle Art von Generatoren sind, wird jedes await durch ein yield irgendwo in der Kette von await-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 in await-Anweisungen zu ermöglichen, nur die Zeile __await__ = __iter__ zur Klasse asyncio.Future hinzugefü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_await definiert 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
ifelse 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

  1. Ein Objekt muss eine Methode __aiter__ implementieren (oder, wenn mit CPython C API definiert, den Slot tp_as_async.am_aiter), die ein asynchrones Iterator-Objekt zurückgibt.
  2. Ein asynchrones Iterator-Objekt muss eine Methode __anext__ implementieren (oder, wenn mit CPython C API definiert, den Slot tp_as_async.am_anext), die ein awaitable zurückgibt.
  3. Um die Iteration zu beenden, muss __anext__ eine Ausnahme StopAsyncIteration auslö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.

  1. Native Coroutinen-Objekte implementieren keine Methoden __iter__ und __next__. Daher können sie nicht iteriert oder an iter(), list(), tuple() und andere eingebaute Funktionen übergeben werden. Sie können auch nicht in einer for..in-Schleife verwendet werden.

    Ein Versuch, __iter__ oder __next__ auf einem nativen Coroutinen-Objekt zu verwenden, führt zu einem TypeError.

  2. Einfache Generatoren können keine nativen Coroutinen yield fromen: Dies führt zu einem TypeError.
  3. Generator-basierte Coroutinen (für asyncio-Code müssen sie mit @asyncio.coroutine [1] dekoriert werden) können native Coroutinen-Objekte yield fromen.
  4. inspect.isgenerator() und inspect.isgeneratorfunction() geben False fü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) gibt True zurück, wenn obj ein natives Coroutinen-Objekt ist.
  • inspect.iscoroutinefunction(obj) gibt True zurück, wenn obj eine native Coroutine-Funktion ist.
  • inspect.isawaitable(obj) gibt True zurück, wenn obj ein awaitable ist.
  • inspect.getcoroutinestate(coro) gibt den aktuellen Zustand eines nativen Coroutinen-Objekts zurück (entspricht inspect.getfgeneratorstate(gen)).
  • inspect.getcoroutinelocals(coro) gibt die Zuordnung der lokalen Variablen eines nativen Coroutinen-Objekts zu ihren Werten zurück (entspricht inspect.getgeneratorlocals(gen)).
  • sys.set_coroutine_wrapper(wrapper) ermöglicht das Abfangen der Erstellung von nativen Coroutinen-Objekten. wrapper muss entweder ein aufrufbares Objekt sein, das ein Argument (ein Coroutine-Objekt) akzeptiert, oder None. None setzt 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. Gibt None zurü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.Awaitable ABC für *Future-ähnliche* Klassen, die die __await__ Methode implementieren.
  • collections.abc.Coroutine ABC für *Coroutine*-Objekte, die die send(value), throw(type, exc, tb), close() und __await__() Methoden implementieren.

    Beachten Sie, dass Generator-basierte Coroutinen mit dem Flag CO_ITERABLE_COROUTINE die Methode __await__ nicht implementieren und daher keine Instanzen der ABCs collections.abc.Coroutine und collections.abc.Awaitable sind.

    @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 def deklariert. Sie verwendet await und return 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.coroutine dekoriert 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 Funktion tp_as_async->am_await, das einen *Iterator* zurückgibt. Kann von einem await-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 mit async with verwendet 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 mit async for verwendet 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 def und NAME Tokens erkennt;
  • während des Tokenisierens eines async def Blocks, ersetzt es das 'async' NAME Token mit ASYNC und das 'await' NAME Token mit AWAIT;
  • während des Tokenisierens eines def Blocks, gibt es 'async' und 'await' NAME Tokens 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

  1. Modifizieren des @asyncio.coroutine Dekorators, um die neue Funktion types.coroutine() zu verwenden.
  2. Hinzufügen der Zeile __await__ = __iter__ zur Klasse asyncio.Future.
  3. Hinzufügen von ensure_future() als Alias für die Funktion async(). Kennzeichnen der Funktion async() 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:

  1. Ein neues Schlüsselwort codef zur Deklaration einer *Cofunction*. Eine *Cofunction* ist immer ein Generator, auch wenn keine cocall Ausdrücke darin vorkommen. Entspricht async def in diesem Vorschlag.
  2. Ein neues Schlüsselwort cocall zum Aufrufen einer *Cofunction*. Kann nur innerhalb einer *Cofunction* verwendet werden. Entspricht await in diesem Vorschlag (mit einigen Unterschieden, siehe unten).
  3. Es ist nicht möglich, eine *Cofunction* ohne das Schlüsselwort cocall aufzurufen.
  4. cocall erfordert grammatikalisch Klammern danach.
    atom: cocall | <existing alternatives for atom>
    cocall: 'cocall' atom cotrailer* '(' [arglist] ')'
    cotrailer: '[' subscriptlist ']' | '.' NAME
    
  5. cocall f(*args, **kwds) ist semantisch äquivalent zu yield from f.__cocall__(*args, **kwds).

Unterschiede zu diesem Vorschlag

  1. Es gibt keine Entsprechung zu __cocall__ in diesem PEP, die aufgerufen wird und deren Ergebnis an yield from im cocall Ausdruck übergeben wird. Das Schlüsselwort await erwartet ein *awaitable* Objekt, validiert den Typ und führt darauf yield from aus. Obwohl die Methode __await__ der Methode __cocall__ ähnelt, wird sie nur zur Definition von *Future-ähnlichen* Objekten verwendet.
  2. await ist in der Grammatik fast genauso definiert wie yield from (später wird durchgesetzt, dass await nur innerhalb von async def vorkommen kann). Es ist möglich, einfach await future zu schreiben, während cocall immer Klammern erfordert.
  3. Damit asyncio mit PEP 3152 funktioniert, müsste der Dekorator @asyncio.coroutine modifiziert 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-in costart(cofunc, *args, **kwargs) verwendet werden.
  4. Da eine *Cofunction* nicht ohne das Schlüsselwort cocall aufgerufen werden kann, verhindert dies automatisch den häufigen Fehler, das yield from für generator-basierte Coroutinen zu vergessen. Dieser Vorschlag adressiert dieses Problem mit einem anderen Ansatz, siehe Debugging-Funktionen.
  5. Ein Manko der Anforderung eines cocall Schlüsselworts zum Aufrufen einer Coroutine ist, dass wenn wir uns entscheiden, Coroutine-Generatoren zu implementieren – Coroutinen mit yield oder async yield Ausdrücken – wir kein cocall Schlü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.
  6. 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))
    
  7. Es gibt keine Entsprechungen zu async for und async with in 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 in async 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

  1. Neue Syntax zur Definition von Coroutinen: async def und neues await Schlüsselwort.
  2. Neue __await__ Methode für Future-ähnliche Objekte und neuer tp_as_async.am_await Slot in PyTypeObject.
  3. Neue Syntax für asynchrone Kontextmanager: async with. Und zugehöriges Protokoll mit den Methoden __aenter__ und __aexit__.
  4. Neue Syntax für asynchrone Iteration: async for. Und zugehöriges Protokoll mit __aiter__, __aexit__ und der neuen eingebauten Ausnahme StopAsyncIteration. Neue Slots tp_as_async.am_aiter und tp_as_async.am_anext in PyTypeObject.
  5. Neue AST-Knoten: AsyncFunctionDef, AsyncFor, AsyncWith, Await.
  6. 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) und inspect.getcoroutinelocals(coro).
  7. Neue Bit-Flags CO_COROUTINE und CO_ITERABLE_COROUTINE für Code-Objekte.
  8. Neue ABCs: collections.abc.Awaitable, collections.abc.Coroutine, collections.abc.AsyncIterable und collections.abc.AsyncIterator.
  9. C API-Änderungen: neuer PyCoro_Type (in Python als types.CoroutineType verfügbar) und PyCoroObject. PyCoro_CheckExact(*o) zum Testen, ob o eine *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.


Quelle: https://github.com/python/peps/blob/main/peps/pep-0492.rst

Zuletzt geändert: 2025-02-01 08:55:40 GMT