Чтобы познакомиться с таблицами обработки исключений, нам потребуется нырнуть глубоко.
Базовая идея: таблица исключений показывает, какие строки байткода покрыты обработчиками ошибок, а какие – нет. По сути – таблица неявных переходов между логическими лейблами. За счет данной технологии реализованы питоновские "zero-cost exceptions".
Возьмем для примера вот такую простую функцию:
def other(x, y):
res = None
try:
res = x / y
except ZeroDivisionError:
res = 0
finally:
print(res)
Получим вот такие: байткод (полная версия по ссылке) и таблицу исключений:
ExceptionTable:
L1 to L2 -> L3 [0]
L3 to L4 -> L6 [1] lasti
L4 to L5 -> L7 [0]
L5 to L6 -> L6 [1] lasti
L6 to L7 -> L7 [0]
L7 to L8 -> L8 [1] lasti
Что она делает?
Если на каком-то участке байткода возникает исключение, то мы прыгаем на нужную логическую метку из таблицы. Пример: в
try (метки c L1 по L2 невключительно) прыгаем на L3.У данной функции будет 3 интересных для нас куска байткода.
Первый – про обработку
finally после успешного случая:
3 L1: LOAD_FAST_BORROW_LOAD_FAST_BORROW 1 (x, y)
BINARY_OP 11 (/)
STORE_FAST 2 (res)
7 L2: LOAD_GLOBAL 3 (print + NULL)
LOAD_FAST_BORROW 2 (res)
CALL 1
POP_TOP
LOAD_CONST 1 (None)
RETURN_VALUE
Тут мы просто делим два объекта и печатаем. Из интересного тут то, что
finally разложился в набор байткода прямо после тела try. Нам не нужно никаких дополнительных манипуляций, чтобы управлять указателем на следующую инструкцию. Так происходит благодаря псевдо-инструкции SETUP_FINALLY. Так было не всегда, раньше тут был JUMP_FORWARD, а finally был общий для всех.Второй – про вызов
finally после обработанного ZeroDivisionError исключения:
-- L3: PUSH_EXC_INFO
4 LOAD_GLOBAL 0 (ZeroDivisionError)
CHECK_EXC_MATCH
POP_JUMP_IF_FALSE 6 (to L5)
NOT_TAKEN
POP_TOP
5 LOAD_SMALL_INT 0
STORE_FAST 2 (res)
L4: POP_EXCEPT
JUMP_BACKWARD_NO_INTERRUPT 28 (to L2)
Тут мы сравниваем тип ошибки, если она
ZeroDivisionError, то обрабатываем, в конце обработки ошибки прыгаем к L2. Если ошибка другая, то прыгаем в L5 (будет ниже). Из интересного, POP_EXCEPT убирает текущее исключение из tstate->exc_info, так исключение считается обработанным. И третий после необработанного исключения:
4 L5: RERAISE 0
-- L6: COPY 3
POP_EXCEPT
RERAISE 1
L7: PUSH_EXC_INFO
7 LOAD_GLOBAL 3 (print + NULL)
LOAD_FAST_CHECK 2 (res)
CALL 1
POP_TOP
RERAISE 0
-- L8: COPY 3
POP_EXCEPT
RERAISE 1
Здесь происходит самое интересное. Нам необходимо правильно обработать ошибки, которые могут случиться в
except и finally блоках. Для такого у нас есть:-
L3 to L4 -> L6 [1] lasti для обработки ошибок в except-
L7 to L8 -> L8 [1] lasti для обработки ошибок в finallyПример, как будут выполняться разные кейсы:
-
other(1, 2): L1 -> L2 (finally)-
other(1, 0): L1 -> L3 -> L4 -> L2 (finally)-
other(1, 'a'): L1 -> L3 (TypeError) -> L5 -> L7 (finally) -> L8Ссылки на исходники:
- RERAISE
- exception_unwind для поиска обработчика текущей ошибки
- get_exception_handler для разворачивания таблицы
Обсуждение: как часто вы пользуетесь
finally в обработке сложных ошибок?| Поддержать | YouTube | GitHub | Чат |