云南网站建设,企业信息化软件定制开发

专业提供昆明网站建设, 昆明软件开发, 云南网站建设,企业信息化软件定制开发服务免费咨询QQ932256355

博客园 首页 新随笔 联系 订阅 管理
  144 随笔 :: 4 文章 :: 3 评论 :: 1192 阅读

深度探索: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 中,函数运行失败时,需设置异常条件并返回错误值(如-1NULL指针)。异常信息存储在解释器线程状态的三个成员中,可通过sys.exc_info()获取对应的 Python 元组。

(二)设置异常的函数

  1. PyErr_SetString:用于设置异常对象和 C 字符串,如PyErr_SetString(PyExc_ZeroDivisionError, "Division by zero occurred");
  2. PyErr_SetFromErrno:根据全局变量errno设置异常描述。
  3. 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 时释放对象。

(二)拥有和借用规则

  1. 拥有规则:函数返回对象引用时,通常传递引用拥有关系,如PyLong_FromLongPy_BuildValue函数。部分函数如PyTuple_GetItemPyList_GetItemPyDict_GetItem等返回借用的引用。当传递对象引用到另一个函数时,通常函数借用引用,若需存储,则使用Py_INCREF将借用引用变为拥有引用。
  2. 借用规则:借用的引用不应调用Py_DECREF,借用者需确保在拥有者处置对象前不再使用该引用。借用引用可通过Py_INCREF变为拥有引用。

九、在 C++ 中编写扩展

在 C++ 中编写扩展模块时,若主程序(Python 解释器)使用 C 编译器编译和链接,全局或静态对象的构造器不能使用;若使用 C++ 编译器链接则无此限制。被 Python 解释器调用的函数(如模块初始化函数)必须声明为extern "C"

十、给扩展模块提供 C API

扩展模块中的代码有时需要被其他扩展模块使用。由于符号可见性在不同操作系统和链接方式下存在差异,为保证可移植性,扩展模块中的符号应声明为static,通过 Capsule 机制传递 C 层级的信息(指针)。Capsule 是存储指针的 Python 数据类型,通过特定的 C API 创建和访问。

十一、实际项目案例

(一)图像识别性能优化项目

在一个图像识别项目中,最初使用纯 Python 实现图像特征提取算法。随着数据量的增加和对实时性要求的提高,Python 代码的性能成为瓶颈。于是,开发团队决定使用 C++ 编写扩展模块来优化关键算法。

  1. 编写 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);
}
  1. 编译与使用:根据项目所在操作系统的要求,选择合适的编译方式将 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(基本线性代数子程序库)来加速矩阵乘法运算。

  1. 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);
}
  1. 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

  1. 官方文档Python 官方文档 - 使用 C 或 C++ 扩展 Python,本文主要参考文档,提供了全面且权威的知识讲解。
  2. Python 源码:Python 的源代码包含了丰富的扩展模块示例,如Modules/xxmodule.c,可在 Python 源码仓库中查看学习,有助于深入理解扩展开发。
  3. 相关书籍:《Python 扩展编程》等书籍对 Python 扩展开发进行了系统讲解,可作为深入学习的参考资料。
posted on   TekinTian  阅读(2)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示