[Python] 扩展程序

本文介绍了在Python中扩展C++代码的必要性,如性能优化和保护私有代码。讨论了扩展的缺点,如需要编写C/C++代码和管理数据转换。详细阐述了扩展过程,包括创建应用代码、编写封装程序和编译测试。同时,推荐使用pybind11作为C++到Python交互的工具,并给出了pybind11的用法,包括类和函数的封装、proto的使用等。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1.Python的C/C++语言扩展学习

注:主要用C语言作为例子扩展python程序

1.1. 什么情况需要扩展python

1)需要 Python 没有的额外功能:比如像创建新的数据类型或在已有应用中嵌入 Python,就必须使用编译后的模块。
2)改善性能:用C或者C++这样运行性能高的语言执行一些耗时程序;但是需要注意的是,我们在用扩展程序对性能优化时,需要找到真正的瓶颈代码,然后用C\C++语言去优化这些瓶颈代码,使其整体运行性能提升。
3)隐藏专有代码:因为python这种易用语言私密性不好,因而,为了保证一些核心代码私有性,用这种扩展的方式,对核心代码的“封装”。因为这些C\C++语言扩展程序是已编译成二进制文件共python调用的,因而安全。

1.2. 扩展python有哪些方法?

1.2.1 Python.h方式

  • Python原生支持的是与C语言的接口,Python的发行版自带有Python.h头文件,里面提供了在C中调用Python和反过来在Python中调用C的接口定义
  • 如果用原生的接口扩展C++会导致很多重复代码,所以这个原生方式 可以考虑用于扩展 C语言

1.2.2 pybind11(推荐使用)

  • 这个是比较常用的,用于扩展C++的三方库,是header-only的库,文档齐全,可以兼容C++很多高级写法。
  • 支持numpy:通过buffer协议来实现的。

1.2.3 Cython

Cython则是基于Python的C/C++代码封装器,其本质也是代码生成器,但是Cython的语法是Python的超集,也就是说Python的代码可以零成本移植到Cython中。

  • 可以支持C++ 模板
  • 无法利用C++的宏定义
  • 如果你的代码需要经常调用封装后的函数,那么选择Cython性能更好(对比pybind11)。

1.2.4 Boost.python

Boost.Python对Numpy的支持比较完备,例如Boost.Python支持自定义numpy.dtype。

1.2.5 SWIG

SWIG提供一个给C++代码编写多种语言绑定的框架,它本质上是一种代码生成器,基于SWIG自定义的语法;

  • 能兼容很多其他语言,JAVA/Ruby/Python等,但是灵活性有了,每个语言的兼容性不是很好。C++的高级特性可能不被很好支持

下面主要讲解Python.h(扩展C常用)和pybind11方式(常用)

3. 通过Python.h扩展C

3.1 大体过程:

1. 创建应用代码:也就是编写对应的C\C++程序;
2. 根据样板编写封装代码:也就是将编写的C\C++程序,封装成:
- 接收python数据类型的数据
- 然后转化为C\C++数据类型数据
- 然后执行C\C++程序
- 将返回结构转化为python数据类型。
的封装函数,用于python调用的接口。
3. 编译\测试: 创建setup,将模块导入python环境下;测试程序的正确性

3.2 用例

3.2.1 创建应用代码
/*myextest.h**/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int fac(int n);
char * reverse(char*s);
int test(void);


/* myextest.c */
#include "myextest.h"
// 阶乘函数
int fac(int n){
    if (n<2) return (1); /* 0!= 1! == 1*/
    return (n) * fac(n-1);
}
// 字符串翻转函数
char * reverse(char*s)
{
    register char t, // temp
             *p = s,
	     *q = (s + (strlen(s) -1));
    while(p < q)
    {
       t = *p;
       *p++ = *q;
       *q-- = t;
    }
    return s;
}
int main(void){ // 需要调试好,确保扩展程序的正确性。
    char s[BUFSIZ];
    printf("4!==%d\n", fac(4));
    strcpy(s,"abcdef");
    printf("reversing 'abcdef', we get '%s'\n", \
          reverse(s));
    return 0;
}// 再次强调,必须尽可能先完善扩展程序的代码。
3.2.2 通过Python.h方式扩展C语言

样板代码主要含有四部分。
1.包含 Python 头文件。
2.为每一个模块函数添加形如 PyObject*Module_func()的封装函数。
3.为每一个模块函数添加一个 PyMethodDef ModuleMethods[]数组/表。
4. 添加模块初始化函数 void initModule()。

/*mywrapper.c*/
#include "Python.h" //要做的是找到 Python 包含文件,并确保编译器可以访问这个文件的目录
#include "myextest.h" 头文件,声明,否则fac和reverse无法被调用
/*说明:
为函数编写形如 PyObject* Module_func()的封装函数
1)需要创建以static PyObject*为返回值的函数
2)函数名必须以模块名开头,紧接着是下划线和函数名本身的函数,注意如果封装函数名字为MyEXT_myfunction();那么python调用时,
from MyEXT import myfunction
怎样才能完成这种转换?答案是在从 Python 到 C 时,调用一系列的PyArg_Parse*()函数,从 C 返回 Python 时,调用 Py_BuildValue()函数。

这些 PyArg_Parse*()函数与 C 中的 sscanf()函数类似。其接受一个字节流,然后根据一些格式字符串进行解析,将结果放入到相应指针所指的变量中。 若解析成功就返回 1; 否则返回 0。
Py_BuildValue()的工作方式类似 sprintf(), 接受一个格式字符串,并将所有参数按照格式字符串指定的格式转换为一个 Python 对象。
*/
static PyObject *
_ext_fac(PyObject*self, PyObject*args){
	int num;
	if (!PyArg_ParseTuple(args,"i",&num))
		return NULL;
	return (PyObject*) Py_BuildValue("i",fac(num));
}
static PyObject*
_ext_reverse_str(PyObject*self, PyObject *args){
	char* orig_str;
	char* dupe_str;
	PyObject * retval;  // return to python
	
	if (!PyArg_ParseTuple(args,"s",&orig_str))
		return NULL;
	//strdup是拷贝一个str, reverse是原地操作,因而为了保留orig_str,需要拷贝后再操作
	retval = (PyObject*)Py_BuildValue("ss", orig_str,dupe_str=reverse(strdup(orig_str))); //此处有拷贝操作,因此需要最后释放原始空间,否则有内存泄露问题。
	free(dupe_str);
	return retval;
}
static PyObject*
_ext_test(PyObject*self, PyObject *args){
	test();
	/* 当创建扩展时,必须额外注意如何处理 Python 对象,必须留心是否
需要修改此类对象的引用计数*/
	Py_INCREF(Py_None);
	return PyNone; //(PyObject*) Py_BuildValue("");
}
/* 为模块编写 PyMethodDef ModuleMethods[]数组
既然两个封装函数都已完成,下一步就需要在某个地方将函数列出来,以便让 Python 解释器知道如何导入并访问这些函数。这就是 ModuleMethods[]数组的任务。
也就是将封装后的函数与python对接,使其能在python中调用
*/
static PyMethodDef
_extMethods[] = 
{
	// {python中访问所用的名称,对应的封装函数,参数以什么形式给定}
	{"fac",_ext_fac,METH_VARARGS}, //METH_VARARGS表示以元组形式给定
	{"reverse_str",_ext_reverse_str,METH_VARARGS},
	{"test",_ext_test,METH_VARARGS},
	{NULL, NULL},
};
/** 添加模块初始化函数 void initModule()
最后一部分是模块初始化函数。当解释器导入模块时会调用这段代码。这段代码中只调用了 Py_InitModule()函数,其第一个参数是模块名称,第二个是ModuleMethods[]数组,这样解释器就可以访问模块函数。
*/
void init_ext(void){
	Py_InitModule("_ext",_extMethods); //初始化所有封装的模块
}
3.2.3 编译

1. 创建 setup.py。
2. 运行 setup.py 来编译并链接代码。
3.在 Python 中导入模块。

#!/usr/bin/env python
/*现在使用distutils 包来构建、安装和发布模块、扩展和软件包*/
from distutils.core import setup, Extension
Mod = '_ext'
ext_modules = [Extension(Mod,sources=['myextest.c','mywrapper.c'])] //注意此处没有.h文件
setup(name=Mod,ext_modules=ext_modules)

编译: python setup.py build,这样可切换到这个目录中进行调用
导入:要么用 python setup.py install导入包,即将其安装到python中,任意目录下使用

from _ext import fac, reverse_str

4.使用pybind11 比较好滴实现C++到python的交互

官方文档:https://readthedocs.org/projects/pybind11/downloads/pdf/stable/ 已经写的比较详细了,可以参考着这个去看

4.1. 一些常用写法说明和记录

// 1) class ClassType {
   
   ....};
PYBIND11_MODULE(module_name, m) {
   
   
// 2) class ClassType {
   
   ...</
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值