Расширения Python C, добавляющие атрибуты в исключение - PullRequest
2 голосов
/ 15 марта 2019

Я обертываю библиотеку C, которая при сбое возвращает один из конечного числа кодов ошибок.Когда происходит ошибка, я хотел бы добавить код ошибки в качестве атрибута исключения C, чтобы код Python мог получить его и сопоставить код ошибки с понятным для человека исключением.Возможно ли это?

Например, я хочу сделать это в слое Python:

try:
    call_my_library_func()
except MyLibraryError as ex:
    print("Error code was %s" % ex.code)

Ближайшее, что мне не нравится, это использование * 1006.*

PyObject *tuple = PyTuple_New(2);
PyTuple_SetItem(tuple, 0, PyUnicode_FromString("Helpful error message"));
PyTuple_SetItem(tuple, 1, PyLong_FromLong(257));
//PyErr_SetString(MyLibraryError, "Helpful error message\n");
PyErr_SetObject(MyLibraryError, tuple);

Тогда я могу сделать это:

try:
    call_my_library_func()
except MyLibraryError as ex:
    message, code = ex.args[0], -1
    if len(ex.args > 1):
        code = ex.args[1]

1 Ответ

1 голос
/ 15 марта 2019

Обработка исключений в C API в основном написана с точки зрения вызова исключения его классом, его аргументами (передаваемыми в конструктор) и его трассировкой, и, следовательно, теми, которые, вероятно, лучше следовать этой схеме.Ваш базовый подход к передаче кортежа в качестве аргументов, вероятно, является наилучшим вариантом.

Однако есть два варианта сделать класс исключений немного более удобным для пользователя на стороне Python:

  1. Вы обрабатываете аргументы в пользовательском методе __init__ для установки атрибута code в классе.
  2. Вы определяете code как свойство вашего класса исключений, которое обращается к args[1].

Я проиллюстрировал вариант 2, но я не думаю, что есть огромная причина предпочесть тот или другой.


Чтобы кратко объяснить пример кода ниже: определитьисключение при использовании C API вы используете PyErr_NewException, который принимает необязательный базовый класс и словарь в качестве второго и третьего аргументов.Используемые функции (либо __init__, либо определения свойств) должны быть частью словаря.

Чтобы определить определения свойств, я написал код на Python и использовал PyRun_String, поскольку его легче писать вPython, а не C и потому что я сомневаюсь, что этот код будет критичным для производительности.Функции в конечном итоге вводятся в глобальный словарь, передаваемый в PyRun_String.

C code:

#include <Python.h>

PyObject* make_getter_code() {
    const char* code = 
    "def code(self):\n"
    "  try:\n"
    "    return self.args[1]\n"
    "  except IndexError:\n"
    "    return -1\n"
    "code = property(code)\n"
    "def message(self):\n"
    "  try:\n"
    "    return self.args[0]\n"
    "  except IndexError:\n"
    "    return ''\n"
    "\n";

    PyObject* d = PyDict_New();
    PyDict_SetItemString(d, "__builtins__", PyEval_GetBuiltins());
    PyObject* output = PyRun_String(code,Py_file_input,d,d);
    if (output==NULL) {
        Py_DECREF(d);
        return NULL;
    }
    Py_DECREF(output);
    PyDict_DelItemString(d,"__builtins__"); /* __builtins__ should not be an attribute of the exception */
    return d;
}

static PyObject* MyLibraryError;

static PyObject* my_library_function(PyObject* self) {
    /* something's gone wrong */
    PyObject *tuple = PyTuple_New(2);
    PyTuple_SetItem(tuple, 0, PyUnicode_FromString("Helpful error message"));
    PyTuple_SetItem(tuple, 1, PyLong_FromLong(257));
    PyErr_SetObject(MyLibraryError, tuple);
    return NULL;
}

static PyMethodDef methods[] = {
    {"my_library_function",  my_library_function,  METH_NOARGS,
     "raise an error."},
    {NULL, NULL, 0, NULL}        /* Sentinel */
};

static struct PyModuleDef librarymodule = {
    PyModuleDef_HEAD_INIT,
    "library",   /* name of module */
    NULL, /* module documentation, may be NULL */
    -1,       /* size of per-interpreter state of the module,
                 or -1 if the module keeps state in global variables. */
    methods
};

PyMODINIT_FUNC
PyInit_library(void) {
    PyObject *m;
    m = PyModule_Create(&librarymodule);
    if (m == NULL)
        return NULL;

    PyObject* exc_dict = make_getter_code();
    if (exc_dict == NULL) {
        return NULL;
    }

    MyLibraryError = PyErr_NewException("library.MyLibraryError", 
                                        NULL, // use to pick base class
                                        exc_dict);
    PyModule_AddObject(m,"MyLibraryError",MyLibraryError);
    return m;
}

В качестве примера более элегантного интерфейса Python ваш код Python изменяется на:

try:
    my_library_func()
except MyLibraryError as ex:
    message, code = ex.message, ex.code

Приложение: можно установить атрибуты для установленного в данный момент исключения.Базовая схема будет выглядеть так:

PyErr_SetString(...) /* raise the exception with an error message */
PyObject *type, *value, *traceback;
PyErr_Fetch(&type, &value, &traceback); /* gets the values of the currently
   set exception and clears it */
PyObject_SetAttrString(value, "code", number);
PyErr_Restore(type,value,traceback); /* Re-sets the exception now including
   the error code */

В этом случае отсутствует проверка ошибок.Я думаю, что я все еще предпочитаю свой подход к помещению логики в класс исключений, но именно так вы бы выполнили операцию, о которой вы просили.

...