8.6. Ausnahmen¶
Ausnahmen innerhalb eines asyncio-Skripts verhalten sich nahezu genauso wie in regulärem Python – sie pflanzen sich die Aufrufkette hinauf fort, bis etwas sie abfängt. Nahezu, weil Aufgaben parallel laufen, sodass der Weg „hinauf“ nicht der Weg ist, der die Aufgabe erstellt hat. Diese Seite behandelt, wohin Ausnahmen in jeder der gängigen Formen gehen.
8.6.1. Innerhalb einer Koroutine¶
Ein try/except innerhalb einer Koroutine fängt Ausnahmen ab, die von allem ausgelöst werden, das sie awaitet, und zwar auf die übliche Weise:
async def fetch_with_retry(url):
for attempt in range(3):
try:
return await fetch(url)
except OSError as e:
last_error = e
raise last_error
Nichts asyncio-Spezifisches hier – die except-Klausel sieht innerhalb von fetch ausgelöste Ausnahmen so, als wäre es ein regulärer Funktionsaufruf gewesen.
8.6.2. In einer Aufgabe, auf die die Anwendung wartet¶
Wenn eine als Task laufende Koroutine eine Ausnahme auslöst, wird die Ausnahme in der Aufgabe gespeichert. Das nächste Mal, wenn etwas diese Aufgabe awaitet, wird die Ausnahme am await erneut ausgelöst:
task = asyncio.create_task(may_fail())
try:
result = await task
except OSError:
log("may_fail failed")
Dasselbe gilt für asyncio.gather(). Das Standardverhalten – ein Kind löst eine Ausnahme aus, die anderen werden abgebrochen, die Ausnahme pflanzt sich aus dem gather hinaus fort – ergibt sich aus diesem Mechanismus.
8.6.3. In einer Aufgabe, auf die niemand wartet¶
Eine Aufgabe, auf die niemand jemals awaitet, ist der Fall, der Aufmerksamkeit erfordert. Die Ausnahme tritt trotzdem auf; die Schleife bemerkt, dass die Aufgabe mit einer unbehandelten Ausnahme beendet wurde; aber es gibt kein await, an dem sie zutage treten könnte. Das Standardverhalten besteht darin, ein Traceback über sys.stderr auszugeben und weiterzulaufen – was für eine unbeaufsichtigte Diagnose in Ordnung ist, aber schlecht zu einer Anwendung passt, die es hätte wissen wollen.
Die richtige Lösung besteht meist darin, die Aufgabe abzuwarten. Entweder direkt, indem man das Handle merkt und es beim Herunterfahren abwartet, oder implizit über gather() oder wait_for(). Das Muster „eine Anwendung herunterfahren“ der Seite Timeouts und Abbruch fängt diesen Fall für die langlebigen Hintergrundaufgaben ab, die ein typisches Skript erzeugt.
8.6.4. Benutzerdefinierter Ausnahmebehandler¶
Wenn stilles Traceback-und-Weiterlaufen nicht ausreicht, stellt die Schleife einen Hook bereit – Loop.set_exception_handler – den die Anwendung überschreiben kann, um etwas anderes zu tun:
def handler(loop, context):
print("asyncio:", context.get("message"))
if "exception" in context:
sys.print_exception(context["exception"])
loop = asyncio.get_event_loop()
loop.set_exception_handler(handler)
Das Argument context ist ein Dict mit den Schlüsseln 'message', 'exception' und 'future'. Die Ausnahme kann bei bestimmten warnungsartigen Ereignissen fehlen, weshalb das Beispiel .get() verwendet.
Typische Verwendungen sind, den Fehler in den Flash zu protokollieren, eine Fehler-LED blinken zu lassen oder zu einem Watchdog-Neustart zu eskalieren. Die Seite loop control behandelt die gesamte Bandbreite der Schleifen-Hooks.
8.6.5. KeyboardInterrupt¶
Wenn ein Skript von außen gestoppt wird – meist dadurch, dass die IDE es zum Anhalten auffordert – trifft die Anfrage innerhalb des Skripts als KeyboardInterrupt ein. Innerhalb von asyncio.run() pflanzt sie sich so fort, wie es jede andere unbehandelte Ausnahme täte: main wird abgebrochen, jede Aufgabe, die die Schleife verfolgt, wird ebenfalls abgebrochen, und der KeyboardInterrupt wird aus asyncio.run() erneut ausgelöst. finally-Klauseln laufen auf dem Weg hinaus, sodass dasselbe Aufräummuster von der Abbruch-Seite es behandelt.