8.6. Kivételek

Az asyncio szkripten belüli kivételek szinte ugyanúgy viselkednek, mint a szokásos Pythonban – felfelé terjednek a hívási láncon, amíg valami el nem kapja őket. Szinte, mert a feladatok párhuzamosan futnak, így a „felfelé” út nem azonos azzal az úttal, amely a feladatot létrehozta. Ez az oldal tárgyalja, hogy a kivételek hová kerülnek az egyes gyakori formákban.

8.6.1. Egyetlen korutinon belül

Egy korutinon belüli try/except a szokásos módon elkapja a bármi által kiváltott kivételeket, amit await-tel megvár:

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

Itt semmi asyncio-specifikus nincs – az except ág úgy látja a fetch belsejében kiváltott kivételeket, mintha az egy szokásos függvényhívás lett volna.

8.6.2. Egy feladatban, amelyet az alkalmazás megvár

Amikor egy Task feladatként futó korutin kivételt vált ki, a kivétel a feladaton tárolódik. Amikor legközelebb valami await-tel megvárja azt a feladatot, a kivétel újra kiváltódik az await helyén:

task = asyncio.create_task(may_fail())
try:
    result = await task
except OSError:
    log("may_fail failed")

Ugyanez vonatkozik az asyncio.gather() hívásra is. Az alapértelmezett viselkedés – az egyik gyermek kivételt vált ki, a többi megszakad, a kivétel pedig kiterjed a gather hívásból – ebből a mechanizmusból ered.

8.6.3. Egy feladatban, amelyet senki sem vár meg

Egy feladat, amelyet soha senki sem vár meg await-tel, az az eset, amely figyelmet igényel. A kivétel ettől még megtörténik; a hurok észreveszi, hogy a feladat kezeletlen kivétellel fejeződött be; de nincs await, ahol felszínre kerülhetne. Az alapértelmezett viselkedés az, hogy egy visszakövetést ír ki a sys.stderr csatornán, és tovább fut – ami egy felügyelet nélküli diagnosztikához rendben van, de gyenge megoldás egy olyan alkalmazáshoz, amely tudni akart róla.

A helyes megoldás általában az, hogy megvárjuk a feladatot. Vagy közvetlenül, a kezelő megjegyzésével és a leállítás során való megvárásával, vagy közvetetten az gather() vagy az wait_for() révén. A Időtúllépések és megszakítás oldal „egy alkalmazás leállítása” mintája elkapja ezt az esetet a hosszú életű háttérfeladatoknál, amelyeket egy tipikus szkript indít.

8.6.4. Egyéni kivételkezelő

Amikor a néma visszakövetés-és-folytatás nem elegendő, a hurok kínál egy horgot – Loop.set_exception_handler –, amelyet az alkalmazás felülírhat, hogy valami mást tegyen:

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)

A context argumentum egy szótár, amelynek kulcsai 'message', 'exception' és 'future'. A kivétel hiányozhat bizonyos figyelmeztetés-jellegű eseményeknél, ezért használja a példa a .get() hívást.

Tipikus felhasználások a hiba flash memóriába naplózása, egy hiba-LED villogtatása, vagy egy felügyeleti újraindításra való eszkaláció. A hurokvezérlés oldal a hurokhorgok teljes felületét tárgyalja.

8.6.5. KeyboardInterrupt

Amikor egy szkriptet kívülről állítanak le – általában úgy, hogy az IDE leállítást kér –, a kérés a szkripten belül egy KeyboardInterrupt formájában érkezik meg. Az asyncio.run() belsejében ugyanúgy terjed, mint bármely más kezeletlen kivétel: a main megszakad, minden feladat, amelyet a hurok nyomon követ, szintén megszakad, és a KeyboardInterrupt újra kiváltódik az asyncio.run() hívásból. A finally ágak lefutnak a kilépés során, így ugyanaz a tisztítási minta kezeli ezt, mint a megszakítási oldalon.