8.6. Wyjątki¶
Wyjątki wewnątrz skryptu asyncio zachowują się niemal tak samo jak w zwykłym Pythonie – propagują się w górę łańcucha wywołań, dopóki coś ich nie przechwyci. Niemal, ponieważ zadania działają równolegle, więc ścieżka „w górę” nie jest ścieżką, która utworzyła zadanie. Ta strona omawia, dokąd trafiają wyjątki w każdej z typowych postaci.
8.6.1. Wewnątrz jednej korutyny¶
try/except wewnątrz korutyny przechwytuje wyjątki zgłoszone przez wszystko, na co awaituje, w zwykły sposób:
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
Nie ma tu nic specyficznego dla asyncio – klauzula except widzi wyjątki zgłoszone wewnątrz fetch tak, jakby było to zwykłe wywołanie funkcji.
8.6.2. W zadaniu, na które aplikacja oczekuje¶
Gdy korutyna działająca jako Task zgłasza wyjątek, jest on przechowywany w zadaniu. Następnym razem, gdy coś awaituje to zadanie, wyjątek jest ponownie zgłaszany przy await
task = asyncio.create_task(may_fail())
try:
result = await task
except OSError:
log("may_fail failed")
To samo dotyczy asyncio.gather(). Domyślne zachowanie – jedno dziecko zgłasza wyjątek, pozostałe zostają anulowane, a wyjątek propaguje się z gather na zewnątrz – wynika z tego mechanizmu.
8.6.3. W zadaniu, na które nikt nie oczekuje¶
Zadanie, na które nikt nigdy nie awaituje, to przypadek wymagający uwagi. Wyjątek nadal się pojawia; pętla zauważa, że zadanie zakończyło się nieobsłużonym wyjątkiem; ale nie ma await, przy którym mógłby się on ujawnić. Domyślnym zachowaniem jest wypisanie śladu stosu przez sys.stderr i kontynuowanie działania – co jest w porządku dla nienadzorowanej diagnostyki, ale słabo pasuje do aplikacji, która chciała o tym wiedzieć.
Właściwym rozwiązaniem jest zwykle oczekiwanie na zadanie. Albo bezpośrednio, zapamiętując uchwyt i oczekując na niego podczas zamykania, albo niejawnie poprzez gather() lub wait_for(). Wzorzec „zamykanie aplikacji” ze strony Limity czasu i anulowanie obsługuje ten przypadek dla długotrwałych zadań w tle, jakie tworzy typowy skrypt.
8.6.4. Niestandardowa obsługa wyjątków¶
Gdy ciche wypisanie śladu stosu i kontynuacja nie wystarcza, pętla udostępnia hak – Loop.set_exception_handler – który aplikacja może nadpisać, aby zrobić coś innego:
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)
Argument context to słownik z kluczami 'message', 'exception' oraz 'future'. Wyjątek może być nieobecny przy pewnych zdarzeniach w stylu ostrzeżeń, dlatego przykład używa .get().
Typowe zastosowania to zapisanie awarii do pamięci flash, mignięcie diodą LED błędu lub eskalacja do restartu przez watchdoga. Strona sterowanie pętlą omawia pełny zakres haków pętli.
8.6.5. KeyboardInterrupt¶
Gdy skrypt zostaje zatrzymany z zewnątrz – zwykle przez IDE proszące o jego zatrzymanie – żądanie dociera do skryptu jako KeyboardInterrupt. Wewnątrz asyncio.run() propaguje się tak, jak każdy inny nieobsłużony wyjątek: main zostaje anulowane, każde zadanie śledzone przez pętlę również zostaje anulowane, a KeyboardInterrupt jest ponownie zgłaszany na zewnątrz asyncio.run(). Klauzule finally wykonują się podczas wychodzenia, więc ten sam wzorzec sprzątania ze strony o anulowaniu jest tym, co go obsługuje.