Python 中的自定义 Error/Exception 类:在 C 扩展中正确派生与抛出
大家好,今天我们来深入探讨一个重要的主题:如何在 Python C 扩展中定义、派生和抛出自定义的 Error/Exception 类。 了解这个主题对于编写健壮、可维护且与 Python 错误处理机制良好集成的 C 扩展至关重要。
为什么需要在 C 扩展中定义自定义异常?
Python 允许我们通过 class 关键字轻松定义自己的异常类。 然而,当涉及到 C 扩展时,事情稍微复杂一些。在 C 扩展中定义自定义异常主要出于以下原因:
- 与 Python 错误处理机制集成: 允许 C 代码向 Python 代码报告特定的错误条件,并且这些错误可以被 Python 的
try...except块捕获和处理。 - 提供更具描述性的错误信息: 自定义异常可以携带额外的信息,比如错误的上下文、状态码等,从而使调试和错误处理更加容易。
- 模块化和组织: 将特定于 C 扩展的错误类型组织到自己的异常类层次结构中,可以提高代码的可读性和可维护性。
- 性能优化: 在某些情况下,在 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 作为我们自定义异常的基类,然后定义了两个子类 ValidationError 和 NetworkError。 ValidationError 携带一个 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;
}
代码解释:
#include <Python.h>: 包含 Python C API 头文件。- *`static PyObject MyCustomError;
:** 声明一个PyObject*类型的变量,用于存储MyCustomError` 类的引用。 这是必要的,以便我们可以在 C 代码中引用这个异常类。 MyCustomError_new:tp_new函数负责分配新的MyCustomError对象的内存。MyCustomError_init:tp_init函数负责初始化新创建的MyCustomError对象。 可以在此添加初始化逻辑,例如设置一些默认属性。MyCustomError_dealloc:tp_dealloc函数负责释放MyCustomError对象占用的内存。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函数的指针。
mymodule:PyModuleDef结构体定义了 C 扩展模块的信息。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;
}
代码解释:
ValidationErrorType.tp_base = &MyCustomErrorType;: 这是关键的一行代码,它将ValidationErrorType的基类设置为MyCustomErrorType。 这意味着ValidationError将继承MyCustomError的所有属性和方法。ValidationError_init:tp_init函数现在解析构造函数参数 "message" 和 "field",并将它们作为属性添加到ValidationError对象。 使用了PyArg_ParseTupleAndKeywords来解析参数,并使用了PyObject_SetAttrString来设置属性。- 模块初始化函数修改: 模块初始化函数现在需要准备
ValidationErrorType并将其添加到模块字典中。
在 C 扩展中抛出异常
要从 C 代码中抛出异常,可以使用 PyErr_SetObject 或 PyErr_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;
}
代码解释:
raise_my_custom_error: 使用PyErr_SetObject抛出MyCustomError异常。 注意必须返回NULL,以便 Python 解释器知道发生了错误。raise_validation_error: 此函数演示了如何使用参数创建ValidationError异常对象,并使用PyErr_SetObject抛出它。- 它首先使用
PyTuple_New和PyDict_New创建一个元组和字典来存储构造函数参数。 - 然后使用
PyDict_SetItemString将 "message" 和 "field" 参数添加到字典中。 - 使用
PyObject_Call调用ValidationErrorType来创建一个新的ValidationError对象。 - 最后,使用
PyErr_SetObject抛出异常。
- 它首先使用
validate_data: 此函数演示了如何在 C 代码中验证数据,并在数据无效时抛出ValidationError异常。MyModuleMethods: 更新模块方法定义,添加了raise_my_custom_error和validate_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_new、tp_init、tp_dealloc 等成员。 |
| 设置基类 | tp_base |
将 PyTypeObject 的 tp_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精英技术系列讲座,到智猿学院