8.6. Eccezioni¶
Le eccezioni all’interno di uno script asyncio si comportano quasi allo stesso modo di quelle in Python normale – si propagano lungo la catena delle chiamate finché qualcosa non le intercetta. Quasi perché i task vengono eseguiti in parallelo, quindi il percorso «verso l’alto» non è il percorso che ha creato il task. Questa pagina illustra dove finiscono le eccezioni in ciascuna delle configurazioni più comuni.
8.6.1. All’interno di una singola coroutine¶
Un try/except all’interno di una coroutine intercetta le eccezioni sollevate da qualsiasi cosa essa awaiti, nel modo consueto:
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
Nulla di specifico di asyncio qui – la clausola except vede le eccezioni sollevate all’interno di fetch come se fosse stata una normale chiamata di funzione.
8.6.2. In un task che l’applicazione sta attendendo¶
Quando una coroutine in esecuzione come Task solleva un’eccezione, questa viene memorizzata nel task. La volta successiva in cui qualcosa awaita quel task, l’eccezione viene ri-sollevata in corrispondenza dell”await
task = asyncio.create_task(may_fail())
try:
result = await task
except OSError:
log("may_fail failed")
Lo stesso vale per asyncio.gather(). Il comportamento predefinito – un figlio solleva un’eccezione, gli altri vengono annullati, l’eccezione si propaga fuori dal gather – deriva da questo meccanismo.
8.6.3. In un task che nessuno attende¶
Un task che nessuno awaita mai è il caso che richiede attenzione. L’eccezione si verifica comunque; il loop si accorge che il task è terminato con un’eccezione non gestita; ma non c’è alcun await in cui possa emergere. Il comportamento predefinito è stampare un traceback tramite sys.stderr e continuare l’esecuzione – il che va bene per una diagnostica non presidiata, ma è poco adatto a un’applicazione che avrebbe voluto saperlo.
La soluzione corretta è di solito attendere il task. Direttamente, ricordando l’handle e attendendolo durante lo spegnimento, oppure implicitamente tramite gather() o wait_for(). Il pattern «spegnere un’applicazione» della pagina Timeout e annullamento gestisce questo caso per i task di background a lunga durata che un tipico script genera.
8.6.4. Gestore di eccezioni personalizzato¶
Quando il traceback-e-continua silenzioso non è sufficiente, il loop espone un hook – Loop.set_exception_handler – che l’applicazione può sovrascrivere per fare qualcos’altro:
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)
L’argomento context è un dict con le chiavi 'message', 'exception' e 'future'. L’eccezione potrebbe essere assente in determinati eventi di tipo warning, motivo per cui l’esempio usa .get().
Gli usi tipici sono registrare il guasto nella flash, far lampeggiare un LED di errore o escalare a un riavvio tramite watchdog. La pagina controllo del loop illustra l’intera gamma di hook del loop.
8.6.5. KeyboardInterrupt¶
Quando uno script viene arrestato dall’esterno – di solito perché l’IDE ne richiede l’interruzione – la richiesta arriva all’interno dello script come KeyboardInterrupt. All’interno di asyncio.run() si propaga come farebbe qualsiasi altra eccezione non gestita: main viene annullata, anche ogni task che il loop sta monitorando viene annullato, e la KeyboardInterrupt viene ri-sollevata fuori da asyncio.run(). Le clausole finally vengono eseguite durante l’uscita, quindi è lo stesso pattern di cleanup della pagina sull’annullamento a gestirla.