深度探索:Python 的 C/C++ 扩展开发实战
本文深入探讨如何使用 C 或 C++ 为 Python 编写扩展模块。详细介绍了从基础概念、编写示例,到错误处理、模块构建,再到 C 与 Python 相互调用、参数处理等多方面的知识,并结合实际项目案例,旨在帮助开发者全面掌握 Python 扩展开发技术,拓展 Python 应用的边界,提升程序性能。
一、引言
Python 作为一门功能强大的编程语言,在很多场景下能高效完成任务。然而,在某些特定需求下,比如实现新的内置对象类型、调用 C 库函数或系统调用时,Python 自身无法直接满足。此时,借助 C 或 C++ 编写扩展模块成为解决方案。通过扩展模块,不仅能突破 Python 的限制,还能充分利用 C/C++ 的高性能优势,优化程序运行效率。
二、Python 扩展开发基础
(一)Python API 与扩展模块
Python 提供了丰富的 API,通过在 C 源文件中引用<Python.h>
头文件即可使用。这些 API 定义了一系列函数、宏和变量,能访问 Python 运行时系统的大部分内容。但需要注意的是,C 扩展接口特定于 CPython,为保持可移植性,在多数情况下应优先考虑ctypes
模块或cffi
库。若必须使用 C 扩展,了解其开发流程至关重要。
(二)简单示例:创建spam
扩展模块
创建一个名为spam
的扩展模块,为 C 库函数system()
创建 Python 接口。首先创建spammodule.c
文件,文件开头需包含#define PY_SSIZE_T_CLEAN
和#include <Python.h>
。
#define PY_SSIZE_T_CLEAN #include <Python.h>
定义spam_system
函数,用于响应spam.system(string)
的调用。
static PyObject *spam_system(PyObject *self, PyObject *args) { const char *command; int sts; if (!PyArg_ParseTuple(args, "s", &command)) return NULL; sts = system(command); return PyLong_FromLong(sts); }
PyArg_ParseTuple
函数用于检查参数类型并转换为 C 值,根据模板字符串s
,它期望接收一个字符串参数,并将其存储到command
变量中。若参数解析失败,返回NULL
,并由PyArg_ParseTuple
设置异常。
三、错误和异常处理
(一)Python 的错误处理惯例
在 Python 中,函数运行失败时,需设置异常条件并返回错误值(如-1
或NULL
指针)。异常信息存储在解释器线程状态的三个成员中,可通过sys.exc_info()
获取对应的 Python 元组。
(二)设置异常的函数
PyErr_SetString
:用于设置异常对象和 C 字符串,如PyErr_SetString(PyExc_ZeroDivisionError, "Division by zero occurred");
。PyErr_SetFromErrno
:根据全局变量errno
设置异常描述。PyErr_SetObject
:设置异常对象和异常描述。
(三)异常检测与清理
使用PyErr_Occurred
检测是否设置了异常。在函数调用链中,若一个函数检测到错误,应返回错误值,而不重复调用PyErr_*
函数,直到错误到达 Python 解释器主循环进行处理。若要忽略异常,可调用PyErr_Clear
。另外,malloc
调用失败时,需调用PyErr_NoMemory
返回错误。
四、模块方法表和初始化函数
(一)模块方法表
定义模块方法表,将spam_system
函数添加到其中。
static PyMethodDef SpamMethods[] = { {"system", spam_system, METH_VARARGS, "Execute a shell command."}, {NULL, NULL, 0, NULL} // Sentinel };
METH_VARARGS
表示函数接受 tuple 格式的参数,使用PyArg_ParseTuple
解析。若函数接受关键字参数,可使用METH_VARARGS | METH_KEYWORDS
,并使用PyArg_ParseTupleAndKeywords
解析参数。
(二)模块定义结构与初始化函数
定义模块定义结构spammodule
,并在初始化函数PyInit_spam
中传递给解释器。
static struct PyModuleDef spammodule = { PyModuleDef_HEAD_INIT, "spam", // 模块名称 NULL, // 模块文档 -1, // 模块的每解释器状态大小 SpamMethods }; PyMODINIT_FUNC PyInit_spam(void) { return PyModule_Create(&spammodule); }
PyModule_Create
函数根据模块定义结构创建模块对象,并插入内置函数对象。若初始化失败,返回NULL
。
五、编译和链接
(一)动态加载
动态加载取决于操作系统的动态加载机制。在不同系统上,编译和链接的方式有所不同,如在 Windows 和 Unix 系统上就存在差异。具体可参考相关系统的 Python 扩展编译文档。
(二)静态链接
若想让模块永久性作为 Python 解释器的一部分,需修改配置设置并重新构建解释器。在 Unix 系统上,将扩展模块源文件(如spammodule.c
)放在Modules/
目录下,在Modules/Setup.local
文件中添加描述行,如spam spammodule.o
,然后在顶层目录运行make
命令重新构建解释器。若模块需要额外链接,可在配置文件中列出,如spam spammodule.o -lX11
。
六、在 C 中调用 Python 函数
(一)传递和保存 Python 函数对象
定义函数用于设置和保存 Python 函数对象的指针,并增加其引用计数。
static PyObject *my_callback = NULL; static PyObject *my_set_callback(PyObject *dummy, PyObject *args) { PyObject *result = NULL; PyObject *temp; if (PyArg_ParseTuple(args, "O:set_callback", &temp)) { if (!PyCallable_Check(temp)) { PyErr_SetString(PyExc_TypeError, "parameter must be callable"); return NULL; } Py_XINCREF(temp); Py_XDECREF(my_callback); my_callback = temp; Py_INCREF(Py_None); result = Py_None; } return result; }
(二)调用 Python 函数
使用PyObject_CallObject
函数调用 Python 函数,传入 Python 函数对象和参数列表(必须是元组对象)。
int arg; PyObject *arglist; PyObject *result; arg = 123; arglist = Py_BuildValue("(i)", arg); result = PyObject_CallObject(my_callback, arglist); Py_DECREF(arglist); if (result == NULL) return NULL; Py_DECREF(result);
调用后需检查返回值是否为NULL
,若为NULL
,表示 Python 函数引发了异常,需根据情况处理。
七、参数处理
(一)提取扩展函数的参数
PyArg_ParseTuple
函数用于提取扩展函数的参数,其声明为int PyArg_ParseTuple(PyObject *arg, const char *format,...)
。arg
为包含参数列表的元组对象,format
为格式字符串,指定参数类型,剩余参数为存储转换后 C 值的变量地址。例如:
int ok; int i, j; long k, l; const char *s; Py_ssize_t size; ok = PyArg_ParseTuple(args, ""); // 无参数 ok = PyArg_ParseTuple(args, "s", &s); // 一个字符串 ok = PyArg_ParseTuple(args, "lls", &k, &l, &s); // 两个长整型和一个字符串
(二)处理关键字参数
PyArg_ParseTupleAndKeywords
函数用于处理带有关键字参数的情况,声明为int PyArg_ParseTupleAndKeywords(PyObject *arg, PyObject *kwdict, const char *format, char *kwlist[],...)
。kwdict
为关键字字典,kwlist
为以NULL
结尾的字符串列表,用于标识形参。
函数 | 作用 | 参数说明 | 适用场景 |
---|---|---|---|
PyArg_ParseTuple |
解析位置参数 | arg :参数元组,format :格式字符串,后续为存储 C 值的变量地址 |
函数只接受位置参数时 |
PyArg_ParseTupleAndKeywords |
解析位置参数和关键字参数 | arg :参数元组,kwdict :关键字字典,format :格式字符串,kwlist :形参标识列表,后续为存储 C 值的变量地址 |
函数接受位置参数和关键字参数时 |
(三)构造任意值
Py_BuildValue
函数用于构造 Python 对象,与PyArg_ParseTuple
类似,但参数为原变量的地址指针,返回适合返回给 Python 代码的 Python 对象。例如:
Py_BuildValue("") // None Py_BuildValue("i", 123) // 123 Py_BuildValue("iii", 123, 456, 789) // (123, 456, 789)
八、引用计数
(一)引用计数原理
Python 通过引用计数管理内存,每个对象都有一个计数器,记录对象引用的数量。当引用计数为 0 时,对象被删除。Py_INCREF(x)
用于增加对象x
的引用计数,Py_DECREF(x)
用于减少引用计数,当引用计数为 0 时释放对象。
(二)拥有和借用规则
- 拥有规则:函数返回对象引用时,通常传递引用拥有关系,如
PyLong_FromLong
和Py_BuildValue
函数。部分函数如PyTuple_GetItem
、PyList_GetItem
、PyDict_GetItem
等返回借用的引用。当传递对象引用到另一个函数时,通常函数借用引用,若需存储,则使用Py_INCREF
将借用引用变为拥有引用。 - 借用规则:借用的引用不应调用
Py_DECREF
,借用者需确保在拥有者处置对象前不再使用该引用。借用引用可通过Py_INCREF
变为拥有引用。
九、在 C++ 中编写扩展
在 C++ 中编写扩展模块时,若主程序(Python 解释器)使用 C 编译器编译和链接,全局或静态对象的构造器不能使用;若使用 C++ 编译器链接则无此限制。被 Python 解释器调用的函数(如模块初始化函数)必须声明为extern "C"
。
十、给扩展模块提供 C API
扩展模块中的代码有时需要被其他扩展模块使用。由于符号可见性在不同操作系统和链接方式下存在差异,为保证可移植性,扩展模块中的符号应声明为static
,通过 Capsule 机制传递 C 层级的信息(指针)。Capsule 是存储指针的 Python 数据类型,通过特定的 C API 创建和访问。
十一、实际项目案例
(一)图像识别性能优化项目
在一个图像识别项目中,最初使用纯 Python 实现图像特征提取算法。随着数据量的增加和对实时性要求的提高,Python 代码的性能成为瓶颈。于是,开发团队决定使用 C++ 编写扩展模块来优化关键算法。
- 编写 C++ 扩展模块:利用 OpenCV 库(一个强大的计算机视觉库,主要用 C++ 编写)在 C++ 扩展模块中实现高效的图像特征提取算法。通过 Python API 将 C++ 函数暴露给 Python 程序调用。
#include <Python.h> #include <opencv2/opencv.hpp> static PyObject* extract_features(PyObject* self, PyObject* args) { const char* image_path; if (!PyArg_ParseTuple(args, "s", &image_path)) { return NULL; } cv::Mat image = cv::imread(image_path); if (image.empty()) { PyErr_SetString(PyExc_RuntimeError, "Could not open or find the image"); return NULL; } // 在这里进行复杂的图像特征提取操作,例如SIFT、SURF等算法 // 简化示例,这里仅返回图像的尺寸信息 int width = image.cols; int height = image.rows; return Py_BuildValue("(ii)", width, height); } static PyMethodDef ImageFeaturesMethods[] = { {"extract_features", extract_features, METH_VARARGS, "Extract image features."}, {NULL, NULL, 0, NULL} }; static struct PyModuleDef imagefeaturesmodule = { PyModuleDef_HEAD_INIT, "imagefeatures", NULL, -1, ImageFeaturesMethods }; PyMODINIT_FUNC PyInit_imagefeatures(void) { return PyModule_Create(&imagefeaturesmodule); }
- 编译与使用:根据项目所在操作系统的要求,选择合适的编译方式将 C++ 代码编译为 Python 扩展模块。在 Python 代码中,通过导入扩展模块调用
extract_features
函数,获取图像特征。
import imagefeatures features = imagefeatures.extract_features('test_image.jpg') print(f"Image width: {features[0]}, height: {features[1]}")
经过优化后,图像特征提取的速度大幅提升,满足了项目的实时性需求。
(二)科学计算加速项目
在一个科学计算项目中,涉及大量的矩阵运算。Python 的numpy
库已经提供了高效的矩阵操作功能,但在某些复杂的计算场景下,仍然需要进一步优化性能。开发人员使用 C 语言编写扩展模块,直接调用 BLAS(基本线性代数子程序库)来加速矩阵乘法运算。
- C 扩展模块实现:在 C 扩展模块中,调用 BLAS 库的函数实现矩阵乘法。
#include <Python.h> #include <numpy/arrayobject.h> #include <cblas.h> static PyObject* matrix_multiply(PyObject* self, PyObject* args) { PyObject* py_matrix1; PyObject* py_matrix2; if (!PyArg_ParseTuple(args, "OO", &py_matrix1, &py_matrix2)) { return NULL; } npy_intp dims1[2], dims2[2]; double* matrix1_data = (double*)PyArray_DATA((PyArrayObject*)py_matrix1); double* matrix2_data = (double*)PyArray_DATA((PyArrayObject*)py_matrix2); int rows1 = PyArray_DIM((PyArrayObject*)py_matrix1, 0); int cols1 = PyArray_DIM((PyArrayObject*)py_matrix1, 1); int cols2 = PyArray_DIM((PyArrayObject*)py_matrix2, 1); dims1[0] = rows1; dims1[1] = cols2; PyObject* result_array = PyArray_SimpleNew(2, dims1, NPY_DOUBLE); double* result_data = (double*)PyArray_DATA((PyArrayObject*)result_array); cblas_dgemm(CblasRowMajor, CblasNoTrans, CblasNoTrans, rows1, cols2, cols1, 1.0, matrix1_data, cols1, matrix2_data, cols2, 0.0, result_data, cols2); return result_array; } static PyMethodDef MatrixMultiplyMethods[] = { {"matrix_multiply", matrix_multiply, METH_VARARGS, "Multiply two matrices."}, {NULL, NULL, 0, NULL} }; static struct PyModuleDef matrixmultiplymodule = { PyModuleDef_HEAD_INIT, "matrixmultiply", NULL, -1, MatrixMultiplyMethods }; PyMODINIT_FUNC PyInit_matrixmultiply(void) { import_array(); return PyModule_Create(&matrixmultiplymodule); }
- Python 调用:在 Python 代码中,导入扩展模块,创建
numpy
矩阵并调用扩展模块中的matrix_multiply
函数进行矩阵乘法运算。
import numpy as np import matrixmultiply matrix1 = np.array([[1, 2], [3, 4]], dtype=np.float64) matrix2 = np.array([[5, 6], [7, 8]], dtype=np.float64) result = matrixmultiply.matrix_multiply(matrix1, matrix2) print(result)
通过这种方式,矩阵乘法的计算速度得到显著提升,在处理大规模矩阵运算时,大大缩短了计算时间。
十二、总结
使用 C 或 C++ 扩展 Python 能为开发者带来诸多优势,但也需要掌握较多的知识和技巧。从基础的 API 使用、模块编写,到复杂的错误处理、内存管理以及 C 与 Python 的交互,每个环节都至关重要。通过实际项目案例可以看到,合理运用 C/C++ 扩展技术,能有效解决 Python 在性能、功能拓展等方面的问题。在实际开发中,应根据具体需求谨慎选择扩展方式,充分考虑可移植性和性能等因素,确保扩展模块的质量和稳定性。
TAG: Python 扩展开发;C/C++ 扩展;Python API;引用计数;Capsule 机制;图像识别优化;科学计算加速
十四、学习资源和 URL
- 官方文档:Python 官方文档 - 使用 C 或 C++ 扩展 Python,本文主要参考文档,提供了全面且权威的知识讲解。
- Python 源码:Python 的源代码包含了丰富的扩展模块示例,如
Modules/xxmodule.c
,可在 Python 源码仓库中查看学习,有助于深入理解扩展开发。 - 相关书籍:《Python 扩展编程》等书籍对 Python 扩展开发进行了系统讲解,可作为深入学习的参考资料。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现