unraisable exceptions в питоне
Мы все с вами привыкли, что в питоне можно "зарайзить" исключение в любой момент: raise Exception
Но, что если в какой-то момент времени мы не можем вызывать исключение?
Простейший пример: что произойдет при запуске такого скрипта?
# ex.py class BrokenDel: def __del__(self): raise ValueError('del is broken') obj = BrokenDel() del obj print('done!') # будет ли выведено?
Тут может быть два варианта:
Или
delвызоветValueErrorи программа завершитсяИли случится какая-то магия, ошибка будет вызвана, напечатается, но программа продолжится
Ну и так как мы с вами на том канале, где мы с вами, то конечно же будет второй вариант.
» python ex.py Exception ignored while calling deallocator : 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
Ошибку мы "вызываем" через специальные АПИ:
Они создают ошибку, но не выкидывают её обычным способом, а сразу отправляют в специальный хук-обработчик. Данный хук не производит классическое "выбрасывание" исключения, а просто его печатает по-умолчанию. Ниже посмотрим, как его можно кастомизировать.
В питоне оно используется где-то 150 раз. То есть – прям часто. Примеры:
Ошибки при завершении интерпретатора, попробуйте сами:
import atexit def foo(): raise Exception('foo') atexit.register(foo)
Ошибки внутри sys.excepthook
Ошибки внутри gc
Ошибки внутри логики установки ошибок (вдруг память кончилась, например) 🌚️️️️
И многое другое
Пользовательское АПИ
Ну и конечно же, есть специальный хук для обработки таких ошибок: sys.unraisablehook
Он может выполнять любой пользовательски�� код, который мы установим при старте приложения.
Например, pytest использует кастомный хук, чтобы валить тесты при возникновении такой ситуации. Что логично.
Нравится контент про технику и устройство технологий? Присоединяйся к каналу @opensource_findings в телеге; там много такого.
Обсуждение: знали ли вы про такую особенность? Приходилось ли где-то в мониторинге особо настраивать?
