8.6. Исключения¶
Исключения внутри asyncio-скрипта ведут себя почти так же, как в обычном Python – они распространяются вверх по цепочке вызовов, пока что-нибудь их не перехватит. Почти потому, что задачи выполняются параллельно, поэтому путь «вверх» – это не тот путь, который создал задачу. На этой странице рассматривается, куда уходят исключения в каждой из распространённых схем.
8.6.1. Внутри одной сопрограммы¶
Конструкция try/except внутри сопрограммы перехватывает исключения, возникшие в любом await-выражении, обычным образом:
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, возбуждает исключение, оно сохраняется в задаче. В следующий раз, когда что-нибудь сделает awaitэтой задачи, исключение будет повторно возбуждено в точке await:
task = asyncio.create_task(may_fail())
try:
result = await task
except OSError:
log("may_fail failed")
То же самое относится к asyncio.gather(). Поведение по умолчанию – один дочерний элемент возбуждает исключение, остальные отменяются, а исключение распространяется наружу из gather – основано на этом механизме.
8.6.3. В задаче, которую никто не ожидает¶
Задача, которую никто никогда не ожидает через await, – это случай, требующий внимания. Исключение всё равно происходит; цикл замечает, что задача завершилась с необработанным исключением; но нет await, в котором оно могло бы всплыть. Поведение по умолчанию – напечатать трассировку через sys.stderr и продолжить выполнение – что подходит для автономной диагностики, но плохо годится для приложения, которое хотело об этом знать.
Правильное решение обычно – ожидать задачу. Либо напрямую, запомнив её дескриптор и ожидая его при завершении работы, либо неявно через 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().
Типичные применения – записать сбой во флеш-память, мигнуть светодиодом ошибки или эскалировать до перезагрузки по сторожевому таймеру. Страница управление циклом охватывает всю поверхность точек расширения цикла.
8.6.5. KeyboardInterrupt¶
Когда скрипт останавливается извне – обычно по запросу IDE на остановку – запрос приходит внутрь скрипта в виде KeyboardInterrupt. Внутри asyncio.run() он распространяется так же, как любое другое необработанное исключение: main отменяется, каждая задача, которую отслеживает цикл, тоже отменяется, и KeyboardInterrupt повторно возбуждается из asyncio.run(). Предложения finally выполняются на выходе, поэтому тот же шаблон очистки со страницы об отмене обрабатывает этот случай.