Python中的自定义Error/Exception类:在C扩展中正确派生与抛出

Python 中的自定义 Error/Exception 类:在 C 扩展中正确派生与抛出

大家好,今天我们来深入探讨一个重要的主题:如何在 Python C 扩展中定义、派生和抛出自定义的 Error/Exception 类。 了解这个主题对于编写健壮、可维护且与 Python 错误处理机制良好集成的 C 扩展至关重要。

为什么需要在 C 扩展中定义自定义异常?

Python 允许我们通过 class 关键字轻松定义自己的异常类。 然而,当涉及到 C 扩展时,事情稍微复杂一些。在 C 扩展中定义自定义异常主要出于以下原因:

  1. 与 Python 错误处理机制集成: 允许 C 代码向 Python 代码报告特定的错误条件,并且这些错误可以被 Python 的 try...except 块捕获和处理。
  2. 提供更具描述性的错误信息: 自定义异常可以携带额外的信息,比如错误的上下文、状态码等,从而使调试和错误处理更加容易。
  3. 模块化和组织: 将特定于 C 扩展的错误类型组织到自己的异常类层次结构中,可以提高代码的可读性和可维护性。
  4. 性能优化: 在某些情况下,在 C 代码中直接处理错误并抛出异常可能比将错误代码返回给 Python 代码进行处理更有效率。

在 Python 中定义异常类

在深入 C 扩展之前,让我们快速回顾一下如何在 Python 中定义异常类。 Python 中的异常类必须直接或间接继承自 BaseException 类。 通常,我们继承自 Exception 类,它是所有内置的非系统退出异常的基类。

class MyCustomError(Exception):
    """自定义异常的基类"""
    pass

class ValidationError(MyCustomError):
    """表示验证错误的异常"""
    def __init__(self, message, field):
        super().__init__(message)
        self.field = field

class NetworkError(MyCustomError):
    """表示网络错误的异常"""
    def __init__(self, message, status_code):
        super().__init__(message)
        self.status_code = status_code

在这个例子中,我们定义了一个 MyCustomError 作为我们自定义异常的基类,然后定义了两个子类 ValidationErrorNetworkErrorValidationError 携带一个 field 属性,指示哪个字段验证失败,而 NetworkError 携带一个 status_code 属性,指示网络请求的状态码。

在 C 扩展中定义异常类

在 C 扩展中定义异常类需要使用 Python C API。 我们需要创建一个 PyTypeObject 结构体来表示我们的异常类,并将其注册到 Python 解释器中。

以下是一个在 C 扩展中定义 MyCustomError 异常类的示例:

#include <Python.h>

// 用于存储 MyCustomError 对象的 PyObject*
static PyObject* MyCustomError;

// MyCustomError 类型的 tp_new 函数
static PyObject*
MyCustomError_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
    return type->tp_alloc(type, 0);
}

// MyCustomError 类型的 tp_init 函数
static int
MyCustomError_init(PyObject *self, PyObject *args, PyObject *kwds)
{
    // 可以添加初始化逻辑,例如设置一些默认属性
    return 0;
}

// MyCustomError 类型的 tp_dealloc 函数
static void
MyCustomError_dealloc(PyObject* self) {
    Py_TYPE(self)->tp_free(self);
}

// MyCustomError 类型的 PyTypeObject
static PyTypeObject MyCustomErrorType = {
    PyVarObject_HEAD_INIT(NULL, 0)
    .tp_name = "my_module.MyCustomError",  // 模块名.类名
    .tp_doc = "Custom error class for my_module",
    .tp_basicsize = sizeof(PyObject),
    .tp_itemsize = 0,
    .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
    .tp_new = MyCustomError_new,
    .tp_init = (initproc)MyCustomError_init,
    .tp_dealloc = (destructor)MyCustomError_dealloc,
};

// 模块初始化函数
static PyModuleDef mymodule = {
    PyModuleDef_HEAD_INIT,
    "my_module",
    "A module with a custom exception",
    -1,
    NULL,
};

PyMODINIT_FUNC
PyInit_my_module(void)
{
    PyObject* m;

    if (PyType_Ready(&MyCustomErrorType) < 0)
        return NULL;

    m = PyModule_Create(&mymodule);
    if (m == NULL)
        return NULL;

    Py_INCREF(&MyCustomErrorType);
    if (PyModule_AddObject(m, "MyCustomError", (PyObject *) &MyCustomErrorType) < 0) {
        Py_DECREF(&MyCustomErrorType);
        Py_DECREF(m);
        return NULL;
    }

    MyCustomError = (PyObject *) &MyCustomErrorType;  // 保存对异常类的引用
    return m;
}

代码解释:

  1. #include <Python.h>: 包含 Python C API 头文件。
  2. *`static PyObject MyCustomError;:** 声明一个PyObject*类型的变量,用于存储MyCustomError` 类的引用。 这是必要的,以便我们可以在 C 代码中引用这个异常类。
  3. MyCustomError_new: tp_new 函数负责分配新的 MyCustomError 对象的内存。
  4. MyCustomError_init: tp_init 函数负责初始化新创建的 MyCustomError 对象。 可以在此添加初始化逻辑,例如设置一些默认属性。
  5. MyCustomError_dealloc: tp_dealloc 函数负责释放 MyCustomError 对象占用的内存。
  6. MyCustomErrorType: PyTypeObject 结构体定义了 MyCustomError 类的类型对象。 关键字段包括:
    • tp_name: 异常类的名称,格式为 "模块名.类名"。
    • tp_doc: 异常类的文档字符串。
    • tp_basicsize: 异常对象的大小,通常是 sizeof(PyObject)
    • tp_flags: 标志,指示类型的行为。 Py_TPFLAGS_DEFAULT 是一个常用的默认值,Py_TPFLAGS_BASETYPE 允许该类型被继承。
    • tp_new: 指向 MyCustomError_new 函数的指针。
    • tp_init: 指向 MyCustomError_init 函数的指针。
    • tp_dealloc: 指向 MyCustomError_dealloc 函数的指针。
  7. mymodule: PyModuleDef 结构体定义了 C 扩展模块的信息。
  8. PyInit_my_module: 模块初始化函数。 当 Python 导入 my_module 模块时,此函数会被调用。 它执行以下操作:
    • PyType_Ready(&MyCustomErrorType): 准备 MyCustomErrorType 类型对象。 这会填充一些默认值并检查错误。
    • PyModule_Create(&mymodule): 创建一个新的 Python 模块。
    • Py_INCREF(&MyCustomErrorType): 增加 MyCustomErrorType 的引用计数。
    • PyModule_AddObject(m, "MyCustomError", (PyObject *) &MyCustomErrorType): 将 MyCustomErrorType 添加到模块的字典中,使其可以通过 my_module.MyCustomError 在 Python 代码中使用。
    • MyCustomError = (PyObject *) &MyCustomErrorType;: 将 MyCustomErrorType 的引用存储到 MyCustomError 变量中,以便我们可以在 C 代码中使用它。

在 C 扩展中派生异常类

要派生一个异常类,比如 ValidationError,我们需要创建一个新的 PyTypeObject,并将其 tp_base 字段设置为基类(例如 MyCustomErrorType)。

#include <Python.h>

// 假设 MyCustomError 的定义和初始化代码与前面的例子相同

// 用于存储 ValidationError 对象的 PyObject*
static PyObject* ValidationError;

// ValidationError 类型的 tp_new 函数
static PyObject*
ValidationError_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
    return type->tp_alloc(type, 0);
}

// ValidationError 类型的 tp_init 函数
static int
ValidationError_init(PyObject *self, PyObject *args, PyObject *kwds)
{
    //  解析参数,例如 message 和 field
    char *message = NULL;
    char *field = NULL;
    static char *kwlist[] = {"message", "field", NULL};

    if (!PyArg_ParseTupleAndKeywords(args, kwds, "ss", kwlist, &message, &field)) {
        return -1;
    }

    //  将 message 和 field 设置为 ValidationError 对象的属性
    PyObject *message_obj = PyUnicode_FromString(message);
    if (message_obj == NULL) {
        return -1;
    }
    if (PyObject_SetAttrString(self, "message", message_obj) < 0) {
        Py_DECREF(message_obj);
        return -1;
    }
    Py_DECREF(message_obj);

    PyObject *field_obj = PyUnicode_FromString(field);
    if (field_obj == NULL) {
        return -1;
    }
    if (PyObject_SetAttrString(self, "field", field_obj) < 0) {
        Py_DECREF(field_obj);
        return -1;
    }
    Py_DECREF(field_obj);

    return 0;
}

// ValidationError 类型的 tp_dealloc 函数
static void
ValidationError_dealloc(PyObject* self) {
    Py_TYPE(self)->tp_free(self);
}

// ValidationError 类型的 PyTypeObject
static PyTypeObject ValidationErrorType = {
    PyVarObject_HEAD_INIT(NULL, 0)
    .tp_name = "my_module.ValidationError",
    .tp_doc = "Validation error class for my_module",
    .tp_basicsize = sizeof(PyObject),
    .tp_itemsize = 0,
    .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
    .tp_new = ValidationError_new,
    .tp_init = (initproc)ValidationError_init,
    .tp_dealloc = (destructor)ValidationError_dealloc,
    .tp_base = &MyCustomErrorType,  //  设置基类
};

// 模块初始化函数 (修改)
PyMODINIT_FUNC
PyInit_my_module(void)
{
    PyObject* m;

    if (PyType_Ready(&MyCustomErrorType) < 0)
        return NULL;

    if (PyType_Ready(&ValidationErrorType) < 0)
        return NULL;

    m = PyModule_Create(&mymodule);
    if (m == NULL)
        return NULL;

    Py_INCREF(&MyCustomErrorType);
    if (PyModule_AddObject(m, "MyCustomError", (PyObject *) &MyCustomErrorType) < 0) {
        Py_DECREF(&MyCustomErrorType);
        Py_DECREF(m);
        return NULL;
    }

    Py_INCREF(&ValidationErrorType);
    if (PyModule_AddObject(m, "ValidationError", (PyObject *) &ValidationErrorType) < 0) {
        Py_DECREF(&ValidationErrorType);
        Py_DECREF(&MyCustomErrorType);
        Py_DECREF(m);
        return NULL;
    }

    MyCustomError = (PyObject *) &MyCustomErrorType;
    ValidationError = (PyObject *) &ValidationErrorType; // 保存引用
    return m;
}

代码解释:

  1. ValidationErrorType.tp_base = &MyCustomErrorType;: 这是关键的一行代码,它将 ValidationErrorType 的基类设置为 MyCustomErrorType。 这意味着 ValidationError 将继承 MyCustomError 的所有属性和方法。
  2. ValidationError_init: tp_init 函数现在解析构造函数参数 "message" 和 "field",并将它们作为属性添加到 ValidationError 对象。 使用了 PyArg_ParseTupleAndKeywords 来解析参数,并使用了 PyObject_SetAttrString 来设置属性。
  3. 模块初始化函数修改: 模块初始化函数现在需要准备 ValidationErrorType 并将其添加到模块字典中。

在 C 扩展中抛出异常

要从 C 代码中抛出异常,可以使用 PyErr_SetObjectPyErr_SetString 函数。 PyErr_SetObject 允许抛出一个自定义异常对象,而 PyErr_SetString 允许抛出一个带有字符串消息的异常。

以下是一些抛出异常的示例:

#include <Python.h>

// 假设 MyCustomError 和 ValidationError 的定义和初始化代码与前面的例子相同

//  一个抛出 MyCustomError 的函数
PyObject*
raise_my_custom_error(void) {
    PyErr_SetObject(MyCustomError, PyUnicode_FromString("Something went wrong!"));
    return NULL; // 必须返回 NULL 以指示发生了错误
}

// 一个抛出 ValidationError 的函数
PyObject*
raise_validation_error(const char* message, const char* field) {
    PyObject *args, *kwds, *error_obj;

    // 构建参数
    args = PyTuple_New(0);
    if (args == NULL) {
        return NULL;
    }

    kwds = PyDict_New();
    if (kwds == NULL) {
        Py_DECREF(args);
        return NULL;
    }

    if (PyDict_SetItemString(kwds, "message", PyUnicode_FromString(message)) < 0) {
        Py_DECREF(args);
        Py_DECREF(kwds);
        return NULL;
    }

    if (PyDict_SetItemString(kwds, "field", PyUnicode_FromString(field)) < 0) {
        Py_DECREF(args);
        Py_DECREF(kwds);
        return NULL;
    }

    // 创建异常对象
    error_obj = PyObject_Call((PyObject*)&ValidationErrorType, args, kwds);
    Py_DECREF(args);
    Py_DECREF(kwds);

    if (error_obj == NULL) {
        // PyObject_Call 可能会设置一个异常,所以直接返回 NULL
        return NULL;
    }

    // 设置异常
    PyErr_SetObject(ValidationError, error_obj);
    Py_DECREF(error_obj);  // 减少引用计数,因为 PyErr_SetObject 会增加引用计数
    return NULL;
}

// 一个调用 raise_validation_error 的示例函数
PyObject*
validate_data(PyObject* self, PyObject* args) {
    const char* data;
    if (!PyArg_ParseTuple(args, "s", &data)) {
        return NULL;
    }

    if (strlen(data) < 5) {
        return raise_validation_error("Data is too short", "data");
    }

    Py_RETURN_NONE; //  验证成功
}

// 模块方法定义 (修改)
static PyMethodDef MyModuleMethods[] = {
    {"raise_my_custom_error", (PyCFunction)raise_my_custom_error, METH_NOARGS, "Raises MyCustomError"},
    {"validate_data", validate_data, METH_VARARGS, "Validates data and raises ValidationError if invalid."},
    {NULL, NULL, 0, NULL}        /* Sentinel */
};

// 模块定义 (修改)
static struct PyModuleDef mymodule = {
    PyModuleDef_HEAD_INIT,
    "my_module",   /* name of module */
    "A module with a custom exception", /* module documentation, may be NULL */
    -1,       /* size of per-interpreter state of the module,
                 or -1 if the module keeps state in global variables. */
    MyModuleMethods
};

// 模块初始化函数 (无需修改,除非需要添加新的异常类型)
PyMODINIT_FUNC
PyInit_my_module(void)
{
    PyObject* m;

    if (PyType_Ready(&MyCustomErrorType) < 0)
        return NULL;

    if (PyType_Ready(&ValidationErrorType) < 0)
        return NULL;

    m = PyModule_Create(&mymodule);
    if (m == NULL)
        return NULL;

    Py_INCREF(&MyCustomErrorType);
    if (PyModule_AddObject(m, "MyCustomError", (PyObject *) &MyCustomErrorType) < 0) {
        Py_DECREF(&MyCustomErrorType);
        Py_DECREF(m);
        return NULL;
    }

    Py_INCREF(&ValidationErrorType);
    if (PyModule_AddObject(m, "ValidationError", (PyObject *) &ValidationErrorType) < 0) {
        Py_DECREF(&ValidationErrorType);
        Py_DECREF(&MyCustomErrorType);
        Py_DECREF(m);
        return NULL;
    }

    MyCustomError = (PyObject *) &MyCustomErrorType;
    ValidationError = (PyObject *) &ValidationErrorType;

    return m;
}

代码解释:

  1. raise_my_custom_error: 使用 PyErr_SetObject 抛出 MyCustomError 异常。 注意必须返回 NULL,以便 Python 解释器知道发生了错误。
  2. raise_validation_error: 此函数演示了如何使用参数创建 ValidationError 异常对象,并使用 PyErr_SetObject 抛出它。
    • 它首先使用 PyTuple_NewPyDict_New 创建一个元组和字典来存储构造函数参数。
    • 然后使用 PyDict_SetItemString 将 "message" 和 "field" 参数添加到字典中。
    • 使用 PyObject_Call 调用 ValidationErrorType 来创建一个新的 ValidationError 对象。
    • 最后,使用 PyErr_SetObject 抛出异常。
  3. validate_data: 此函数演示了如何在 C 代码中验证数据,并在数据无效时抛出 ValidationError 异常。
  4. MyModuleMethods: 更新模块方法定义,添加了 raise_my_custom_errorvalidate_data 方法。

在 Python 中捕获异常

现在我们已经定义了自定义异常并在 C 扩展中抛出了它们,我们可以在 Python 代码中使用 try...except 块来捕获它们。

import my_module

try:
    my_module.raise_my_custom_error()
except my_module.MyCustomError as e:
    print(f"Caught MyCustomError: {e}")

try:
    my_module.validate_data("abc")
except my_module.ValidationError as e:
    print(f"Caught ValidationError: {e}, field: {e.field}, message: {e.message}")

表格总结:关键步骤和函数

步骤 C 函数/结构体 描述
定义异常类 PyTypeObject 定义异常类的类型对象,包括名称、文档字符串、大小、标志、tp_newtp_inittp_dealloc 等成员。
设置基类 tp_base PyTypeObjecttp_base 成员设置为基类的 PyTypeObject 指针。
初始化异常对象 tp_init 解析构造函数参数,并将它们设置为异常对象的属性。
抛出异常 PyErr_SetObject, PyErr_SetString 设置当前线程的异常。 PyErr_SetObject 允许抛出一个自定义异常对象,而 PyErr_SetString 允许抛出一个带有字符串消息的异常。
模块初始化 PyModule_AddObject, PyType_Ready 将异常类添加到模块字典中,并准备类型对象。
参数解析(可选) PyArg_ParseTuple, PyArg_ParseTupleAndKeywords 解析传递给 C 函数的 Python 参数。
设置对象属性(可选) PyObject_SetAttrString 设置 Python 对象的属性。
创建对象实例 (可选) PyObject_Call 调用一个 Python 可调用对象,例如一个类,来创建一个新的对象实例。

注意事项

  • 引用计数: 始终注意 Python 对象的引用计数。 使用 Py_INCREF 增加引用计数,使用 Py_DECREF 减少引用计数。 未正确管理引用计数会导致内存泄漏或程序崩溃。PyErr_SetObject 会增加异常对象的引用计数,所以抛出异常后,通常需要 Py_DECREF 异常对象。
  • 错误处理: 在 C 代码中始终检查错误。 如果 C API 函数返回错误,请确保正确处理错误并抛出 Python 异常。
  • 命名约定: 遵循 Python 的命名约定。 异常类应以 Error 结尾。
  • 文档: 为自定义异常类编写清晰的文档字符串。

总结:定义、派生和抛出自定义异常

这篇文章详细介绍了在 Python C 扩展中定义、派生和抛出自定义异常的步骤。 通过正确地定义和使用自定义异常,可以使 C 扩展与 Python 的错误处理机制无缝集成,从而提高代码的健壮性、可读性和可维护性。掌握这些概念对于编写高质量的 Python C 扩展至关重要。

更多IT精英技术系列讲座,到智猿学院

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注