Обновить

unraisable exceptions в питоне

Мы все с вами привыкли, что в питоне можно "зарайзить" исключение в любой момент: 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 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 в телеге; там много такого.

Обсуждение: знали ли вы про такую особенность? Приходилось ли где-то в мониторинге особо настраивать?

Теги:
Всего голосов 9: ↑8 и ↓1+10
Комментарии1

Публикации