当前位置: 首页 > 科技观察

如何将C-C++程序编译成实用的Python模块

时间:2023-03-20 17:54:23 科技观察

Python遇到性能瓶颈怎么办?答案就是找一个具有相应功能的C/C++程序,编译成CPython模块,用于Python调用,提高性能。如何将C/C++程序编译成Python模块。例如在用于科学计算的Python中,用于数据处理的Numpy模块是用C语言编写的。Numpy的处理速度是Pandas的数倍。Numpy的处理速度并不比go语言差。本文主要介绍如何将C/C++程序编译成Python模块。本文技术性较强,需要耐心阅读。作为一种胶水语言,Python可以很容易地通过C/C++进行扩展以提高性能。之前写过一篇文章,介绍如何通过Python的ctypes加载常用的.so库。事实上,这并不是真正用C/C++编写的Python扩展模块。本文将介绍如何使用C语言和C++编写Python模块。1、Python的C语言接口Python语言最初是一种用C语言实现的脚本语言,后来因为后来用它的语言实现了Python而被称为CPython,比如用Python实现的Python——PyPy,用Java语言实现的Python——Jython,Python由.Net-IronPython实现。CPython具有优良的开放性和可扩展性,提供了方便灵活的应用程序编程接口(API),使C/C++程序员能够扩展Python解释器的功能。Python的C语言接口非常适合封装各种用C语言实现的功能。如果要封装C++类,使用boost_python或者SWIG更方便更合适。还有一个类似于boost_python的pybind11,支持C++11。1模块封装假设我们有一个C函数:/*文件名:mylib.c*/intaddone(inta){returna+1;}如果要在Python解释器中调用这个函数,首先应该将其实现为一个模块,其中需要编写相应的包接口,如下:(args,"i:fact",&n))returnNULL;result=addone(n);/*这里调用了C函数*/returnPy_BuildValue("i",result);}staticPyMethodDefmylibMethods[]={{"addone",wrap_addone,METH_VARARGS,"AddonetoN"},{NULL,NULL}};voidinitmylib(){PyObject*m;m=Py_InitModule("mylib",mylibMethods);}以上是一个典型的Python扩展模块,至少应该包含三部分:导出函数、方法列表和初始化函数。2导出函数要在Python解释器中调用C语言的函数,首先要为其编写相应的导出函数,上例中的导出函数为wrap_addone。在Python的C语言扩展中,所有导出的函数都有相同的函数原型:PyObject*wrap_method(PyObject*self,PyObject*args);thisfunction是Python解释器和C函数的接口,一般用wrapped_开头后跟C语言的函数名,这样名字就和导出的函数和C语言函数一一对应,使代码更清晰。它有两个参数:self和args。参数self仅在C函数作为内置方法实现时使用,通常该参数的值为空(NULL)。参数args包含了Python解释器将传递给C函数的所有参数,通常使用Python的C语言扩展接口提供的函数PyArg_ParseTuple()来获取这些参数值。所有导出的函数都返回一个PyObject指针。如果对应的C函数没有真正的返回值(即返回值类型为void),则应该返回一个全局的None对象(Py_None),并将其引用计数加1,如下所示:PyObject*wrap_method(PyObject*self,PyObject*args){Py_INCREF(Py_None);returnPy_None;}3方法列表方法列表列出了Python解释器可以使用的所有方法。上面例子对应的方法列表是:staticPyMethodDefmylibMethods[]={{"addone",wrap_addone,METH_VARARGS,"AddonetoN"},{NULL,NULL}};方法列表中的每一项由四部分组成:方法名导出函数参数传递方法方法描述方法名是从Python解释器调用方法时使用的名称。参数传递方式规定了Python向C函数传递参数的具体形式。两个可选的方法是METH_VARARGS和METH_KEYWORDS,其中METH_VARARGS是参数传递的标准形式,通过Python元组在Python解释器和C函数之间传递。如果使用METH_KEYWORD方法,Python解释器和C函数会通过Python字典类型在两者之间传递参数。4初始化函数所有的Python扩展模块都必须有一个初始化函数,这样Python解释器才能正确地初始化模块。Python解释器规定所有初始化函数的函数名必须以init开头,加上模块名。对于模块mylib,对应的初始化函数为:voidinitmylib(){PyObject*m;m=Py_InitModule("mylib",mylibMethods);}当Python解释器需要导入模块时,会根据module对应的初始化函数,一旦找到,就会调用执行相应的初始化工作,初始化函数会通过调用Python的C语言扩展接口提供的函数Py_InitModule(),向Python解释器注册模块中所有可用的模块。方法。5编译链接要在Python解释器中使用C语言编写的扩展模块,必须编译成动态链接库的形式。下面以Linux为例介绍如何将C语言编写的Python扩展模块编译成动态链接库:$gcc-fpic-shared-omylib.so-I/usr/include/python2.7mylib.cwrap_mylib.c6调用上面在Python中编译好的Python扩展模块的动态链接库可以直接在Python中导入。如下:veelion@gtx:~$pythonPython2.7.12(default,Nov192016,06:48:10)[GCC5.4.020160609]onlinux2Type"help","copyright","credits"or"license"formoreinformation.>>>importexample>>>example.addone(7)8>>>>>>这里生成的.so动态库和上一篇没有Python的C语言生成的动态库不同,从生成过程和使用方法可以看到这里的动态库使用起来感觉就像一个Python模块,直接导入即可。2.使用boost_python库封装C++类安装boostpython库:sudoaptitudeinstalllibboost-python-devexample下面的代码简单实现了一个常用函数maxab()和一个Student类:#include#includeintmaxab(inta,intb){returna>b?a:b;}classStudent{private:intage;std::stringname;public:Student(){}Student(std::stringconst&_name,int_age){name=_name;age=_age;}staticvoidmyrole(){std::cout<<"我是学生!"<#include#include#include“student.h”使用命名空间boost::python;BOOST_PYTHON_MODULE(student){//Thiswillenableuser-defineddocstringsandpythonsignatures,//whiledisablingtheC++signaturesscope().attr("__version__")="1.0.0";scope().attr("__doc__")="ademomoduletoseboost_python.";docstring_optionslocal_docstring_options(true,false,false);def("maxab",&maxab,"returnmaxoftwonumbers.");class_("Student","aclassofstudent").def(init<>()).def(init())//methodsforChinesewordsegmentation.def("whoami",&Student::whoami,"method'sdocstring...").def("myrole",&Student::myrole,"method'sdocstring...").staticmethod("myrole");//封装STLclass_("StudentVec").def(vector_indexing_suite());}上面的代码还是包含了Python.h文件,如果不包含会报错:wrap_python.hpp:50:23:fatalerror:pyconfig.h:Nosuchfileordirectory上面的代码有两种编译方式,一种在命令行使用g++直接编译:g++-I/usr/include/python2.7-fPICwrap_student.cpp-lboost_python-shared-ostudent.so先指定Python.h的路径,如果是Python3,则一定要修改成对应的路径,编译wrap_student.cpp指定-fPIC参数,链接(-lboost_python)生成动态库(-shared)生成的student.so动态库可以直接用python导入。In[1]:importstudentIn[2]:student.maxab(2,5)Out[2]:5In[3]:s=student.Student('Tom',12)In[4]:s.whoami()IamTomIn[5]:s.myrole()我是学生!另一种方法是使用python的setuptools编写setup.py脚本:#!/usr/bin/envpythonfromsetuptoolsimportsetup,Extensionsetup(name="student",ext_modules=[Extension("student",["wrap_student.cpp"],libraries=["boost_python"])])然后执行命令编译:pythonsetup.pybuildorsudopythonsetup.pyinstall三、SWIG除了boost_python,封装C++类Python调用C/C++代码的利器还有SWIG(SimplifiedWrapperandInterfaceGenerator),它是一种用于调用脚本语言的C和C++程序的软件开发工具。它实际上是一个编译器,得到C/C++的声明和定义,并用shell封装起来,以便其他脚本语言可以访问这些声明。因此,SWIG最大的优势就是将脚本语言的开发效率和C/C++的运行效率有机结合起来。双数组TrieTree的一个实现:cedar可以用于中文分词、新词发现等算法中的词典创建。本文使用cedar的SWIG包实现来说明SWIG的使用。0.安装swig工欲善其事必先利其器。您必须先安装swig。在Ubuntu上安装swig非常简单:sudoaptitudeinstallswig1。声明和定义C/C++代码。在cedar.h的swig目录下有cedar的C++声明和实现代码trie,但是这个实现中没有遍历所有key的函数方法,所以我加了一个实现,先定义一个数据结构来定义key://key-valuepairreturntypefornext_key()classkv_t{public:std::stringkey;intvalue;};添加一个函数,每次返回一个key。当key字符串为空时,表示遍历结束。如果继续调用,会从头开始遍历://toiterateallkeyskv_tnext_key()const{staticsize_tfrom=0,p=0;union{inti;intx;}b;charkey[256]={0};kv_tkv;if(from==0){b.i=_t->begin(from,p);}else{b.i=_t->next(from,p);}if(b.i==trie_t::CEDAR_NO_PATH){kv.key="";kv.value=0;from=0;p=0;returnkv;}_t->suffix(key,p,from);kv.key=key;kv.value=b.x;returnkv;}2.编写接口文件。我查看cedar.i可以看到SWIG接口文件的写法:首先在%module之后声明模块名,也就是Python导入时使用的模块名;include%{...%}之间的相关头文件%include之后可以声明对STL的支持,最后声明要封装的函数和变量,也可以include头文件:%include"trie.h"3.打包代码见Makefilepython-bindings:python-bindings:swig-Wall-python-builtin-outdirpython-c++cedar.imv-fcedar_wrap.cxxpython直接make或者单独运行上面的swig命令,即可生成割让ar.py和cedar_wrap.cxx文件4.编译生成动态库编译后的cedar_wrap.cxx使用了pythondistutils的setup,可以参考python/setup.py的编写。setup.py的构建如下:pythonsetup.pybuild会在当前目录下创建一个目录build,下面会生成lib.linux-x86_64-2.7/cedar.py和_cedar.so。4.pybind11packageC++从名字就可以看出pybind11是一个将C++11代码包装成Python模块的库。它的目标和用法类似于Boost_python库,但比庞大的Boost库更加精简。很久不认识这个库了,也没有实践过。以前都是写C++,然后用boost打包的。不过个人感觉pybind11更加简洁,下次项目可以试试。届时,我将与您分享我的经验。