Antwort auf die Frage
In Python werden Coroutines, die über async def erstellt werden, als einmal verwendbare Zustandsautomaten implementiert, die auf Bytecode-Ebene in CPython durch die CO_ITERABLE_COROUTINE oder native Coroutine-Flags gesteuert werden. Wenn Sie eine asynchrone Funktion aufrufen, gibt sie sofort ein Coroutine-Objekt zurück, das ein Frame-Objekt und den Ausführungszustand enthält; das Warten auf das Objekt treibt diesen Zustandsautomaten bis zur Vollziehung, an dem Punkt der interne f_lasti (letzte Instruktion) Marker das Ende erreicht und das Frame als erschöpft gekennzeichnet wird. Die Python-Laufzeit verhindert ausdrücklich ein erneutes Eintreten, indem sie dieses Vollzugsflag überprüft und einen RuntimeError auslöst, wenn nachfolgende Awaits erfolgen, da Coroutines so gestaltet sind, dass sie einzelne, diskrete asynchrone Operationen mit linearer Kontrollfluss darstellen. Im Gegensatz dazu sind Generatorfunktionen Fabriken – jeder Aufruf erzeugt ein brandneues PyGenObject mit dem eigenen unabhängigen Stack-Frame und dem Befehlszeiger, was es der Funktion ermöglicht, mehrere unabhängige Iteratoren zu erzeugen, die jeweils separate Ausführungskontexte beibehalten.
Situation aus dem Leben
Ein Entwicklungsteam baute einen widerstandsfähigen WebSocket-Client, der fehlgeschlagene Verbindungsversuche mit exponentiellem Backoff erneut versuchen musste. Sie definierten zunächst eine Verbindungs-Coroutine auf Modulebene und versuchten, sie über die Retry-Logik wiederzuverwenden.
import asyncio async def establish_connection(): return await websockets.connect("wss://api.example.com") # Instanziierung auf Modulebene connection_coro = establish_connection() async def retry_connect(max_attempts=3): for attempt in range(max_attempts): try: ws = await connection_coro # Scheitert bei der zweiten Iteration return ws except Exception: await asyncio.sleep(2 ** attempt)
Das Problem trat auf, als die zweite Schleifeniteration versuchte, connection_coro erneut abzuwarten, was einen RuntimeError auslöste, da das erste erfolgreiche Await bereits das Coroutine-Objekt erschöpft hatte. Das Team prüfte drei architektonische Lösungen.
Ein Ansatz bestand darin, das Coroutine-Objekt manuell im Except-Block nach Auffangen des RuntimeError neu zu konstruieren. Obwohl dies technisch machbar war, führte es zu fragiler Zustandsverwaltung und machte den Code abhängig von der Erkennung der Erschöpfung über Ausnahmebehandlung, was semantisch mehrdeutig ist und legitime Laufzeitfehler innerhalb der Verbindungslogik verschleiern könnte.
Eine andere Lösung schlug vor, establish_connection in eine Klasse umzuwandeln, die __await__ implementiert, um ein zurücksetzbares Awaitable zu erstellen. Dies bot ein Fabrikmuster, fügte jedoch unnötigen Boilerplate und Komplexität hinzu, verschleierte die einfache Absicht, eine Verbindung herzustellen und erforderte manuelle Zustandsverfolgung, die das, was die Python-Laufzeit bereits durch Funktionsaufrufe bereitstellt, duplizierte.
Die gewählte Lösung bestand darin, die asynchrone Funktion als Fabrik zu behandeln, indem der Aufrufstandort in die Schleife verschoben wurde, sodass jede Iteration ein frisches Coroutine-Objekt erhielt. Durch das Refactoring zu ws = await establish_connection() wurde bei jedem Versuch ein neuer Zustandsautomat mit unabhängiger Ressourcenverwaltung instanziiert. Dies entsprach der Designphilosophie von Python, wo asynchrone Funktionen Konstruktoren für einmalige rechnerische Zukünfte sind, was zu sauberen, ausnahmefreien Retry-Logiken führte, die fehlgeschlagene Verbindungsversuche ordentlich von nachfolgenden Wiederholungen isolierten.
Was Kandidaten oft übersehen
Warum führt das Speichern einer Coroutine in einer Variablen und das Vergessen, darauf zu warten, zu einem Ressourcenleck, und wie mildert close() dies?
Kandidaten nehmen oft an, dass unbeachtete Coroutines einfach ohne Nebeneffekte Müll abräumen. Wenn eine Coroutine jedoch ihren Körper betreten und an einem await-Ausdruck suspendiert wurde (z.B. eine Datenbankverbindung oder ein Sperrobjekt haltend), behält das Frame Referenzen auf diese Ressourcen. Das Aufrufen von close() auf dem Coroutine-Objekt zwingt eine GeneratorExit-Ausnahme durch das Frame, was Kontexthandler (async with) und try/finally-Blöcke dazu bringt, Ressourcen sofort freizugeben. Ohne explizites close() bleiben diese Ressourcen gehalten, bis die zyklische Müllabfuhr durchgeführt wird, was für Szenarien einer Erschöpfung des Verbindungs-Pools zu spät sein könnte.
Wie unterscheidet sich inspect.iscoroutine() von inspect.isawaitable(), und warum ist diese Unterscheidung wichtig, wenn man generische asyncio-Hilfsfunktionen schreibt?
inspect.iscoroutine() gibt nur für native Coroutine-Objekte, die von async def-Funktionen erstellt wurden, True zurück, während inspect.isawaitable() True für jedes Objekt zurückgibt, das __await__ implementiert, einschließlich Coroutines, Tasks, Futures und benutzerdefinierter Awaitables. Kandidaten übersehen, dass asyncio-Funktionen wie ensure_future() jede awaitable akzeptieren, nicht nur Coroutines. Das Schreiben von Bibliotheken, die strikt iscoroutine() überprüfen, lehnt gültige Awaitables wie asyncio.Queue().get() oder benutzerdefinierte Zukunftsobjekte ab und bricht die Polymorphie in generischen Hilfsfunktionen, die willkürliche asynchrone Operationen planen sollen.
Was ist der Unterschied zwischen async for und await, wenn man einen asynchronen Generator konsumiert, und warum muss letzterer __aiter__ zurückgeben, um den Generator selbst und nicht eine Coroutine zurückzugeben?
await konsumiert eine Coroutine oder Future bis zur Vollziehung und gibt einen einzelnen Wert zurück, während async for über einen asynchronen Iterator iteriert und an jedem yield innerhalb einer async def-Generatorfunktion pausiert. Kandidaten verwechseln async for mit dem Warten auf eine Liste von Coroutines. Von entscheidender Bedeutung ist, dass __aiter__ das asynchrone Iterator-Objekt direkt (nicht ein Awaitable) zurückgeben muss, weil die Python-Laufzeit __aiter__ synchron aufruft, um den Iterator vor Beginn des Iterationsprotokolls zu erhalten. Das Zurückgeben einer Coroutine von __aiter__ verursacht einen TypeError, da das Protokoll sofortigen Zugang zur Methode __anext__ des Iterator's erwartet, um den Zustand des asynchronen Iterationszustandsautomaten zu steuern.