8.6. Винятки¶
Винятки всередині скрипту asyncio поводяться майже так само, як у звичайному Python – вони поширюються вгору по ланцюжку викликів, поки щось їх не перехопить. Майже тому, що задачі виконуються паралельно, тому шлях «вгору» не є шляхом, який створив задачу. Ця сторінка описує, куди потрапляють винятки в кожній з поширених конфігурацій.
8.6.1. Всередині однієї корутини¶
A try/except inside a coroutine catches exceptions raised by anything it awaits, in the usual way:
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
Нічого специфічного для asyncio тут немає – блок except бачить винятки, згенеровані всередині fetch, так само, ніби це був звичайний виклик функції.
8.6.2. У задачі, яку програма очікує¶
Коли корутина, що виконується як Task, генерує виняток, цей виняток зберігається на задачі. Наступного разу, коли щось awaits цю задачу, виняток повторно генерується на await
task = asyncio.create_task(may_fail())
try:
result = await task
except OSError:
log("may_fail failed")
Те саме стосується asyncio.gather(). Стандартна поведінка – одна дочірня задача генерує виняток, інші скасовуються, виняток поширюється за межі gather – є наслідком цього механізму.
8.6.3. У задачі, яку ніхто не очікує¶
Задача, яку ніхто ніколи awaits – це той випадок, що потребує уваги. Виняток все одно виникає; цикл помічає, що задача завершилася з необробленим винятком; але немає await, на якому він міг би спливти. Стандартна поведінка – вивести трасування стеку через sys.stderr і продовжувати роботу – що прийнятно для неприсутньої діагностики, але погано підходить для програми, яка хотіла знати про це.
Правильне виправлення зазвичай полягає в тому, щоб await задачу. Або безпосередньо, запам’ятавши дескриптор і await його під час завершення, або неявно через gather() або wait_for(). Шаблон «завершення програми» зі сторінки Таймаути та скасування охоплює цей випадок для довгоживучих фонових задач, які породжує типовий скрипт.
8.6.4. Власний обробник винятків¶
Коли мовчазного виведення трасування і продовження роботи недостатньо, цикл надає хук – Loop.set_exception_handler – який програма може перевизначити для іншої дії:
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)
Аргумент context – це словник з ключами 'message', 'exception' і 'future'. Виняток може бути відсутнім у деяких попереджувальних подіях, тому в прикладі використовується .get().
Типові варіанти використання – записати збій на флеш-пам’ять, мигнути LED помилки або ескалувати до перезапуску через watchdog. Сторінка керування циклом охоплює всю поверхню хуків циклу.
8.6.5. KeyboardInterrupt¶
Коли скрипт зупиняється ззовні – зазвичай за запитом IDE зупинити його – запит надходить всередину скрипту як KeyboardInterrupt. Всередині asyncio.run() він поширюється так само, як і будь-який інший необроблений виняток: main скасовується, кожна задача, яку відстежує цикл, також скасовується, і KeyboardInterrupt повторно генерується за межами asyncio.run(). Блоки finally виконуються на виході, тому той самий шаблон очищення зі сторінки скасування обробляє цей випадок.