PEP 525 – Asynchrone Generatoren
- Autor:
- Yury Selivanov <yury at edgedb.com>
- Discussions-To:
- Python-Dev Liste
- Status:
- Final
- Typ:
- Standards Track
- Erstellt:
- 28. Jul. 2016
- Python-Version:
- 3.6
- Post-History:
- 02. Aug. 2016, 23. Aug. 2016, 01. Sep. 2016, 06. Sep. 2016
Zusammenfassung
PEP 492 führte Unterstützung für native Koroutinen und die async/await Syntax in Python 3.5 ein. Hier wird vorgeschlagen, die asynchronen Fähigkeiten von Python durch die Unterstützung für *asynchrone Generatoren* zu erweitern.
Begründung und Ziele
Reguläre Generatoren (eingeführt in PEP 255) ermöglichten eine elegante Möglichkeit, komplexe *Datenproduzenten* zu schreiben und sie wie einen Iterator zu behandeln.
Derzeit gibt es jedoch kein gleichwertiges Konzept für das *asynchrone Iterationsprotokoll* (async for). Dies macht das Schreiben asynchroner Datenproduzenten unnötig komplex, da man eine Klasse definieren muss, die __aiter__ und __anext__ implementiert, um sie in einer async for-Anweisung verwenden zu können.
Im Wesentlichen gelten die Ziele und die Begründung von PEP 255, angewendet auf den asynchronen Ausführungsfall, auch für diesen Vorschlag.
Leistung ist ein zusätzlicher Punkt für diesen Vorschlag: In unseren Tests der Referenzimplementierung sind asynchrone Generatoren **2x** schneller als eine äquivalente Implementierung als asynchroner Iterator.
Als Veranschaulichung der Verbesserung der Codequalität betrachten wir die folgende Klasse, die Zahlen mit einer gegebenen Verzögerung ausgibt, sobald sie iteriert wird
class Ticker:
"""Yield numbers from 0 to `to` every `delay` seconds."""
def __init__(self, delay, to):
self.delay = delay
self.i = 0
self.to = to
def __aiter__(self):
return self
async def __anext__(self):
i = self.i
if i >= self.to:
raise StopAsyncIteration
self.i += 1
if i:
await asyncio.sleep(self.delay)
return i
Dasselbe kann als viel einfacherer asynchroner Generator implementiert werden
async def ticker(delay, to):
"""Yield numbers from 0 to `to` every `delay` seconds."""
for i in range(to):
yield i
await asyncio.sleep(delay)
Spezifikation
Dieser Vorschlag führt das Konzept der *asynchronen Generatoren* in Python ein.
Diese Spezifikation setzt Kenntnisse der Implementierung von Generatoren und Koroutinen in Python voraus (PEP 342, PEP 380 und PEP 492).
Asynchronous Generators
Ein Python-*Generator* ist jede Funktion, die einen oder mehrere yield-Ausdrücke enthält
def func(): # a function
return
def genfunc(): # a generator function
yield
Wir schlagen vor, denselben Ansatz zu verwenden, um *asynchrone Generatoren* zu definieren
async def coro(): # a coroutine function
await smth()
async def asyncgen(): # an asynchronous generator function
await smth()
yield 42
Das Ergebnis des Aufrufs einer *asynchronen Generatorfunktion* ist ein *asynchrones Generatorobjekt*, das das in PEP 492 definierte asynchrone Iterationsprotokoll implementiert.
Es ist ein SyntaxError, eine nicht leere return-Anweisung in einem asynchronen Generator zu haben.
Unterstützung für das asynchrone Iterationsprotokoll
Das Protokoll erfordert die Implementierung von zwei speziellen Methoden
- Eine
__aiter__-Methode, die einen *asynchronen Iterator* zurückgibt. - Eine
__anext__-Methode, die ein *awaitable*-Objekt zurückgibt, das dieStopIteration-Ausnahme verwendet, um Werte zu "yielden", und dieStopAsyncIteration-Ausnahme, um das Ende der Iteration zu signalisieren.
Asynchrone Generatoren definieren beide dieser Methoden. Lassen Sie uns manuell über einen einfachen asynchronen Generator iterieren
async def genfunc():
yield 1
yield 2
gen = genfunc()
assert gen.__aiter__() is gen
assert await gen.__anext__() == 1
assert await gen.__anext__() == 2
await gen.__anext__() # This line will raise StopAsyncIteration.
Finalisierung
PEP 492 erfordert eine Event-Schleife oder einen Scheduler, um Koroutinen auszuführen. Da asynchrone Generatoren aus Koroutinen verwendet werden sollen, benötigen sie ebenfalls eine Event-Schleife, um sie auszuführen und zu finalisieren.
Asynchrone Generatoren können try..finally-Blöcke sowie async with haben. Es ist wichtig, eine Garantie zu geben, dass Generatoren, selbst wenn sie teilweise iteriert und dann vom Garbage Collector aufgeräumt werden, sicher finalisiert werden können. Zum Beispiel
async def square_series(con, to):
async with con.transaction():
cursor = con.cursor(
'SELECT generate_series(0, $1) AS i', to)
async for row in cursor:
yield row['i'] ** 2
async for i in square_series(con, 1000):
if i == 100:
break
Der obige Code definiert einen asynchronen Generator, der async with verwendet, um über einen Datenbankcursor in einer Transaktion zu iterieren. Der Generator wird dann mit async for iteriert, was die Iteration an einem bestimmten Punkt unterbricht.
Der Generator square_series() wird dann vom Garbage Collector aufgeräumt, und ohne einen Mechanismus zum asynchronen Schließen des Generators könnte der Python-Interpreter nichts tun.
Um dieses Problem zu lösen, schlagen wir Folgendes vor
- Implementierung einer
aclose-Methode für asynchrone Generatoren, die ein spezielles *awaitable* zurückgibt. Wenn dies awaited wird, wirft es eineGeneratorExitin den pausierten Generator und iteriert darüber, bis entweder eineGeneratorExitoder eineStopAsyncIterationauftritt.Dies ähnelt stark dem, was die
close()-Methode für reguläre Python-Generatoren tut, nur dass eine Event-Schleife zur Ausführung vonaclose()benötigt wird. - Auslösen einer
RuntimeError, wenn ein asynchroner Generator einenyield-Ausdruck in seinemfinally-Block ausführt (die Verwendung vonawaitist jedoch in Ordnung)async def gen(): try: yield finally: await asyncio.sleep(1) # Can use 'await'. yield # Cannot use 'yield', # this line will trigger a # RuntimeError.
- Hinzufügen von zwei neuen Methoden zum
sys-Modul:set_asyncgen_hooks()undget_asyncgen_hooks().
Die Idee hinter sys.set_asyncgen_hooks() ist es, Event-Schleifen zu ermöglichen, die Iteration und Finalisierung von asynchronen Generatoren abzufangen, so dass der Endbenutzer sich nicht um das Finalisierungsproblem kümmern muss und alles einfach funktioniert.
sys.set_asyncgen_hooks() akzeptiert zwei Argumente
firstiter: ein Callable, das aufgerufen wird, wenn ein asynchroner Generator zum ersten Mal iteriert wird.finalizer: ein Callable, das aufgerufen wird, wenn ein asynchroner Generator kurz vor dem Garbage Collection steht.
Wenn ein asynchroner Generator zum ersten Mal iteriert wird, speichert er eine Referenz auf den aktuellen *Finalizer*.
Wenn ein asynchroner Generator kurz vor dem Garbage Collection steht, ruft er seinen gespeicherten *Finalizer* auf. Es wird davon ausgegangen, dass der Finalizer einen aclose()-Aufruf mit der Schleife plant, die zum Zeitpunkt des Beginns der Iteration aktiv war.
Hier ist zum Beispiel, wie asyncio modifiziert wird, um eine sichere Finalisierung von asynchronen Generatoren zu ermöglichen
# asyncio/base_events.py
class BaseEventLoop:
def run_forever(self):
...
old_hooks = sys.get_asyncgen_hooks()
sys.set_asyncgen_hooks(finalizer=self._finalize_asyncgen)
try:
...
finally:
sys.set_asyncgen_hooks(*old_hooks)
...
def _finalize_asyncgen(self, gen):
self.create_task(gen.aclose())
Das zweite Argument, firstiter, ermöglicht es Event-Schleifen, eine schwache Menge von asynchronen Generatoren zu verwalten, die unter ihrer Kontrolle instanziiert wurden. Dies ermöglicht die Implementierung von "Shutdown"-Mechanismen, um alle offenen Generatoren sicher zu finalisieren und die Event-Schleife zu schließen.
sys.set_asyncgen_hooks() ist threadspezifisch, sodass mehrere parallel laufende Event-Schleifen es sicher verwenden können.
sys.get_asyncgen_hooks() gibt eine Namedtuple-ähnliche Struktur mit den Feldern firstiter und finalizer zurück.
asyncio
Die asyncio Event-Schleife verwendet die sys.set_asyncgen_hooks() API, um eine schwache Menge aller geplanten asynchronen Generatoren zu verwalten und ihre aclose() Koroutinen-Methoden zu planen, wenn die Generatoren vom Garbage Collector aufgeräumt werden sollen.
Um sicherzustellen, dass asyncio-Programme alle geplanten asynchronen Generatoren zuverlässig finalisieren können, schlagen wir die Hinzufügung einer neuen Event-Schleifen-Koroutinenmethode loop.shutdown_asyncgens() vor. Diese Methode plant die Schließung aller aktuell offenen asynchronen Generatoren mit einem aclose()-Aufruf.
Nachdem die Methode loop.shutdown_asyncgens() aufgerufen wurde, gibt die Event-Schleife eine Warnung aus, wann immer ein neuer asynchroner Generator zum ersten Mal iteriert wird. Die Idee ist, dass nach der Aufforderung zur Beendigung aller asynchronen Generatoren das Programm keinen Code ausführen sollte, der über neue asynchrone Generatoren iteriert.
Ein Beispiel dafür, wie die Koroutine shutdown_asyncgens verwendet werden sollte
try:
loop.run_forever()
finally:
loop.run_until_complete(loop.shutdown_asyncgens())
loop.close()
Asynchrones Generatorobjekt
Das Objekt ist dem Standard-Python-Generatorobjekt nachempfunden. Im Wesentlichen ist das Verhalten asynchroner Generatoren so konzipiert, dass es das Verhalten synchroner Generatoren nachbildet, wobei der einzige Unterschied darin besteht, dass die API asynchron ist.
Die folgenden Methoden und Eigenschaften sind definiert
agen.__aiter__(): Gibtagenzurück.agen.__anext__(): Gibt ein *awaitable* zurück, das eine asynchrone Generator-Iteration durchführt, wenn es awaited wird.agen.asend(val): Gibt ein *awaitable* zurück, das das Objektvalin denagen-Generator pusht. Wennagennoch nicht iteriert wurde, mussvalNonesein.Beispiel
async def gen(): await asyncio.sleep(0.1) v = yield 42 print(v) await asyncio.sleep(0.2) g = gen() await g.asend(None) # Will return 42 after sleeping # for 0.1 seconds. await g.asend('hello') # Will print 'hello' and # raise StopAsyncIteration # (after sleeping for 0.2 seconds.)
agen.athrow(typ, [val, [tb]]): Gibt ein *awaitable* zurück, das eine Ausnahme in denagen-Generator wirft.Beispiel
async def gen(): try: await asyncio.sleep(0.1) yield 'hello' except ZeroDivisionError: await asyncio.sleep(0.2) yield 'world' g = gen() v = await g.asend(None) print(v) # Will print 'hello' after # sleeping for 0.1 seconds. v = await g.athrow(ZeroDivisionError) print(v) # Will print 'world' after # sleeping 0.2 seconds.
agen.aclose(): Gibt ein *awaitable* zurück, das eineGeneratorExit-Ausnahme in den Generator wirft. Das *awaitable* kann entweder einen ausgegebenen Wert zurückgeben, wennagendie Ausnahme behandelt hat, oderagenwird geschlossen und die Ausnahme wird an den Aufrufer weitergegeben.agen.__name__undagen.__qualname__: lesbare und beschreibbare Namen und qualifizierte Namensattribute.agen.ag_await: Das Objekt, auf dasagengerade *wartet*, oderNone. Dies ist ähnlich wie das derzeit verfügbaregi_yieldfromfür Generatoren undcr_awaitfür Koroutinen.agen.ag_frame,agen.ag_runningundagen.ag_code: ähnlich wie die entsprechenden Attribute von Standardgeneratoren definiert.
StopIteration und StopAsyncIteration werden nicht aus asynchronen Generatoren weitergegeben und durch eine RuntimeError ersetzt.
Implementierungsdetails
Das asynchrone Generatorobjekt (PyAsyncGenObject) teilt sich das Struktur-Layout mit PyGenObject. Zusätzlich führt die Referenzimplementierung drei neue Objekte ein
PyAsyncGenASend: das awaitable-Objekt, das die Methoden__anext__undasend()implementiert.PyAsyncGenAThrow: das awaitable-Objekt, das die Methodenathrow()undaclose()implementiert._PyAsyncGenWrappedValue: jedes direkt von einem asynchronen Generator ausgegebene Objekt wird implizit in diese Struktur verpackt. So kann die Generatorimplementierung Objekte, die über das reguläre Iterationsprotokoll ausgegeben werden, von Objekten trennen, die über das asynchrone Iterationsprotokoll ausgegeben werden.
PyAsyncGenASend und PyAsyncGenAThrow sind awaitable (sie haben __await__-Methoden, die self zurückgeben) und sind koroutinenähnliche Objekte (die die Methoden __iter__, __next__, send() und throw() implementieren). Im Wesentlichen steuern sie, wie asynchrone Generatoren iteriert werden
PyAsyncGenASend und PyAsyncGenAThrow
PyAsyncGenASend ist ein koroutinenähnliches Objekt, das die Methoden __anext__ und asend() antreibt und das asynchrone Iterationsprotokoll implementiert.
agen.asend(val) und agen.__anext__() geben Instanzen von PyAsyncGenASend zurück (die Referenzen zurück zum übergeordneten agen-Objekt halten).
Der Datenfluss ist wie folgt definiert
- Wenn
PyAsyncGenASend.send(val)zum ersten Mal aufgerufen wird, wirdvalan das übergeordneteagen-Objekt gepusht (unter Verwendung der bestehenden Einrichtungen vonPyGenObject).Nachfolgende Iterationen über die
PyAsyncGenASend-Objekte pushenNoneanagen.Wenn ein Objekt vom Typ
_PyAsyncGenWrappedValueausgegeben wird, wird es entpackt, und eineStopIteration-Ausnahme wird mit dem entpackten Wert als Argument ausgelöst. - Wenn
PyAsyncGenASend.throw(*exc)zum ersten Mal aufgerufen wird, wird*excin das übergeordneteagen-Objekt geworfen.Nachfolgende Iterationen über die
PyAsyncGenASend-Objekte pushenNoneanagen.Wenn ein Objekt vom Typ
_PyAsyncGenWrappedValueausgegeben wird, wird es entpackt, und eineStopIteration-Ausnahme wird mit dem entpackten Wert als Argument ausgelöst. return-Anweisungen in asynchronen Generatoren lösen eineStopAsyncIteration-Ausnahme aus, die über die MethodenPyAsyncGenASend.send()undPyAsyncGenASend.throw()weitergegeben wird.
PyAsyncGenAThrow ist dem PyAsyncGenASend sehr ähnlich. Der einzige Unterschied besteht darin, dass PyAsyncGenAThrow.send(), wenn es zum ersten Mal aufgerufen wird, eine Ausnahme in das übergeordnete agen-Objekt wirft (anstatt einen Wert hineinzupushen).
Neue Funktionen und Typen der Standardbibliothek
types.AsyncGeneratorType– Typ des asynchronen Generatorobjekts.sys.set_asyncgen_hooks()undsys.get_asyncgen_hooks()Methoden zur Einrichtung von Finalizern und Iterations-Interceptors für asynchrone Generatoren in Event-Schleifen.inspect.isasyncgen()undinspect.isasyncgenfunction()Introspektionsfunktionen.- Neue Methode für die asyncio Event-Schleife:
loop.shutdown_asyncgens(). - Neue abstrakte Basisklasse
collections.abc.AsyncGenerator.
Abwärtskompatibilität
Der Vorschlag ist vollständig abwärtskompatibel.
In Python 3.5 ist es ein SyntaxError, eine Funktion mit async def zu definieren, die einen yield-Ausdruck enthält. Daher ist es sicher, asynchrone Generatoren in 3.6 einzuführen.
Performance
Reguläre Generatoren
Für reguläre Generatoren gibt es keine Leistungseinbußen. Der folgende Mikro-Benchmark läuft auf CPython mit und ohne asynchrone Generatoren mit derselben Geschwindigkeit
def gen():
i = 0
while i < 100000000:
yield i
i += 1
list(gen())
Verbesserungen gegenüber asynchronen Iteratoren
Der folgende Mikro-Benchmark zeigt, dass asynchrone Generatoren etwa **2,3x schneller** sind als asynchrone Iteratoren, die in reinem Python implementiert sind
N = 10 ** 7
async def agen():
for i in range(N):
yield i
class AIter:
def __init__(self):
self.i = 0
def __aiter__(self):
return self
async def __anext__(self):
i = self.i
if i >= N:
raise StopAsyncIteration
self.i += 1
return i
Designüberlegungen
aiter() und anext() Builtins
Ursprünglich definierte PEP 492 __aiter__ als eine Methode, die ein *awaitable*-Objekt zurückgeben sollte, was zu einem asynchronen Iterator führte.
In CPython 3.5.2 wurde __aiter__ jedoch neu definiert, um direkt asynchrone Iteratoren zurückzugeben. Um die Abwärtskompatibilität zu wahren, wurde beschlossen, dass Python 3.6 beide Wege unterstützt: __aiter__ kann immer noch ein *awaitable* mit einer DeprecationWarning zurückgeben.
Aufgrund dieser doppelten Natur von __aiter__ in Python 3.6 können wir keine synchrone Implementierung des Built-in aiter() hinzufügen. Daher wird vorgeschlagen, bis Python 3.7 zu warten.
Asynchrone Listen-/Dict-/Set-Comprehensions
Die Syntax für asynchrone Comprehensions steht in keinem Zusammenhang mit der Mechanik asynchroner Generatoren und sollte in einer separaten PEP behandelt werden.
Asynchrones yield from
Obwohl es theoretisch möglich ist, die Unterstützung für yield from für asynchrone Generatoren zu implementieren, würde dies eine ernsthafte Neugestaltung der Generatorimplementierung erfordern.
yield from ist auch für asynchrone Generatoren weniger kritisch, da kein Mechanismus zur Implementierung eines weiteren Koroutinenprotokolls aufbauend auf Koroutinen bereitgestellt werden muss. Und um asynchrone Generatoren zu komponieren, kann eine einfache async for-Schleife verwendet werden
async def g1():
yield 1
yield 2
async def g2():
async for v in g1():
yield v
Warum die Methoden asend() und athrow() notwendig sind
Sie ermöglichen die Implementierung von Konzepten, die contextlib.contextmanager ähneln, mithilfe asynchroner Generatoren. Zum Beispiel ist es mit dem vorgeschlagenen Design möglich, das folgende Muster zu implementieren
@async_context_manager
async def ctx():
await open()
try:
yield
finally:
await close()
async with ctx():
await ...
Ein weiterer Grund ist, dass es möglich ist, Daten in asynchrone Generatoren zu pushen und Ausnahmen über das Objekt zu werfen, das von __anext__ zurückgegeben wird, aber es ist schwierig, dies korrekt zu tun. Das Hinzufügen von expliziten asend() und athrow() ebnet einen sicheren Weg, dies zu erreichen.
In Bezug auf die Implementierung ist asend() eine leicht generischere Version von __anext__, und athrow() ist aclose() sehr ähnlich. Daher fügt die Definition dieser Methoden für asynchrone Generatoren keine zusätzliche Komplexität hinzu.
Beispiel
Ein funktionierendes Beispiel mit der aktuellen Referenzimplementierung (druckt Zahlen von 0 bis 9 mit einer Sekunde Verzögerung)
async def ticker(delay, to):
for i in range(to):
yield i
await asyncio.sleep(delay)
async def run():
async for i in ticker(1, 10):
print(i)
import asyncio
loop = asyncio.get_event_loop()
try:
loop.run_until_complete(run())
finally:
loop.close()
Akzeptanz
PEP 525 wurde von Guido am 6. September 2016 angenommen [2].
Implementierung
Die Implementierung wird in Issue 28003 verfolgt [3]. Das Git-Repository der Referenzimplementierung ist verfügbar unter [1].
Referenzen
Danksagungen
Ich danke Guido van Rossum, Victor Stinner, Elvis Pranskevichus, Nathaniel Smith, Łukasz Langa, Andrew Svetlov und vielen anderen für ihr Feedback, ihre Code-Reviews und Diskussionen rund um diese PEP.
Urheberrecht
Dieses Dokument wurde gemeinfrei erklärt.
Quelle: https://github.com/python/peps/blob/main/peps/pep-0525.rst
Zuletzt geändert: 2025-02-01 08:59:27 GMT