Мы все с вами привыкли, что в питоне можно "зарайзить" исключение в любой момент:
raise ExceptionНо, что если в какой-то момент времени мы не можем вызывать исключение?
Простейший пример: что произойдет при запуске такого скрипта?
# ex.py
class BrokenDel:
def __del__(self):
raise ValueError('del is broken')
obj = BrokenDel()
del obj
print('done!') # будет ли выведено?
Тут может быть два варианта:
1. Или
del вызовет ValueError и программа завершится2. Или случится какая-то магия, ошибка будет вызвана, напечатается, но программа продолжится
Ну и так как мы с вами на том канале, где мы с вами, то конечно же будет второй вариант.
» ./python.exe ex.py
Exception ignored while calling deallocator <function BrokenDel.__del__ at 0x10303c1d0>:
Traceback (most recent call last):
File "/Users/sobolev/Desktop/cpython/ex.py", line 3, in __del__
raise ValueError('del is broken')
ValueError: del is broken
done!
Знакомьтесь – unraisable exceptions 🤝
Как оно работает?
В некоторых местах C кода у нас есть необходимость вызывать исключения, но нет технической возможности. Пример, как выглядит упрощенный
dealloc для list?
static void
list_dealloc(PyListObject *op)
{
Py_ssize_t i;
PyObject_GC_UnTrack(op); // убираем объект из отслеживания gc
if (op->ob_item != NULL) {
i = Py_SIZE(op);
while (--i >= 0) {
Py_XDECREF(op->ob_item[i]); // уменьшаем счетчик ссылок каждого объекта в списке
}
op->ob_item = NULL;
}
PyObject_GC_Del(op);
}
А, как вы можете знать, чтобы в C коде вызвать ошибку, нужно сделать две вещи:
- Взывать специальное АПИ вроде
PyErr_SetString(PyExc_ValueError, "some text")- И вернуть
NULL как PyObject * из соответствующих АПИ, показывая, что у нас ошибка. Если вернуть NULL нельзя, то мы не можем поставить ошибку. А тут у нас void и вернуть вообще ничего нельзя. Потому приходится использовать вот такой подход с unraisable exceptionОшибку мы "вызываем" через специальные АПИ:
- PyErr_WriteUnraisable
- PyErr_FormatUnraisable
Они создают ошибку, но не выкидывают её обычным способом, а сразу отправляют в специальный хук-обработчик.
В питоне оно используется где-то 150 раз. То есть – прям часто. Примеры:
- Ошибки при завершении интерпретатора, попробуйте сами:
import atexit
def foo():
raise Exception('foo')
atexit.register(foo)
- Ошибки внутри sys.excepthook
- Ошибки внутри
gc- Ошибки внутри логики установки ошибок (вдруг память кончилась, например) 🌚️️️️
- И многое другое
Пользовательское АПИ
Ну и конечно же, есть специальный хук для обработки таких ошибок: sys.unraisablehook
Например, pytest использует кастомный хук, чтобы валить тесты при возникновении такой ситуации. Что логично.
Обсуждение: знали ли вы про такую особенность? Приходилось ли где-то в мониторинге особо настраивать?
| Поддержать | YouTube | GitHub | Чат |