Boost Python编译Extension module
[[Boost]]是C++一个广泛使用的第三方库,虽然是第三方库,但是实际上已经成为一个不亚于标准库的标准实现,其中包含了容器、算法、日期、序列化等多种功能的子模块。<boost/python.hpp>是Boost提供的C++和Python的紧密互操作性的模块。
本文的示例代码可以在[]yangrq1018/boost-python-blog-demo (github.com)获得
众所周知,如今最流行的Python实现是CPython,即基于C语言实现的Python运行时环境。因为这个原因,只要遵守一系列CPython规定的接口的C或者C++动态库(DLL),其中的C/C++函数和类就可以被导入到Python代码中使用,以此解决纯Python代码无法满足的一些性能需求。
引出函数
举个例子,假如我们对Python里的加法的运行速度感到不满,想要实现自己的加法
1 | int add(const int& a, const int& b) { |
将add暴露给Python在Boost里很简单,使用boost::python::def导出一个C++函数为Python函数,函数名为"add",声明了两个参数a和b
1 |
|
宏BOOST_PYTHON_MODULE的第一个参数定义了Python模块的名字mymath。Python对dynamic module定义的一组接口函数需要正确匹配模块的名字。如果你想要最终在Python里通过from mymath import XXX,则BOOST_PYTHON_MODULE需要得到正确的模块名以正确生成这些接口。
然后我们使用[[XMake]]编译项目
1 | -- require boost and boost python |
引入boost依赖时,
shared配置为false会静态链接boost,否则默认为动态链接。动态链接情况下需要将boost的dll文件拷贝到python文件的统一目录下,否则import mymath会报“找不到指定的模块,静态链接情况下需要的依赖则都打包进.pyd`中,只需要拷贝这个扩展库就可以在python中引用。
在Python里导入的方式跟普通的Python模块一样
1 | # ensure `mymath.pyd` is on the proper path relative to this script |
可以看到add函数的文档也一并生成了
1 | print(add.__doc__) |
Python是一门弱类型的语言而C++是一门强类型的语言(每个变量都必须声明唯一且不可变的类型),如果在Python中以错误的参数类型调用
add,例如add("foo", "bar"),会触发ArgumentError。
函数重载
C++中的函数/方法往往有多个重载,以支持对不同参数类型的灵活支持,而Python是没有函数重载这个特性的,因为参数本身并没有类型限制,那么我们如何把C++的一组函数重载导出到Python的同一个函数下,而且完美支持响应的函数类型调用呢?如果你尝试直接将重载函数名传给boost::python::def,会提示编译错误
1 | int add(const int& a, const int& b) { |
原因是def并没有办法解析一个有多个版本的重载函数名,使之匹配到单独一个Python函数上,这是做不到的,必须手动地把每一个重载版本交给def。
1 | int add(const int& a, const int& b) { |
引出类
假如我们在C++中写了一个Student类
1 | class Student { |
使用boost::python::class_模板引出class Student
1 |
|
通过init<>()我们绑定了Student的无参数构造函数(绑定到__init__方法)
属性Property
add_property将name属性的getter绑定到Student::name方法上,注意我们传递的是函数(方法)指针。
修改xmake.lua增加一个新的target
1 | -- ... |
在Python中使用Student就和普通的Python类型一样
1 | from student import Student |
add_property也支持接受一个方法作为属性的setter,和Python的property.setter装饰器有类似的作用。
1 | class Student { |
函数和Special methods (__XXX__)
接下来,绑定普通方法learn,__str__, __repr__等magic method
1 | class Student { |
self_ns::str(self),self_ns::repr(self)和self+self这样的绑定看起来十分怪异,但其实他并没有什么复杂的东西,self_ns::str代表self_ns(self namespace)命名空间下的stroperator,仅此而已,将其传递给def会使得boost寻找对应的operator overload来绑定到python相应的operator上- 其实上述方法不是绑定
__str__和__repr__的唯一选择,也可以通过def("__str__", method, doc)这样的形式绑定,但这种情况下method需要是一个返回值是std::string的方法,而非operator<<这样的函数构造函数和__init__
C++中类有多个重载的构造函数很常见,绑定多个构造函数后,在Python中可以通过1
2
3
4
5
6
7
8
9
10
11
12
13
14class Student {
public:
Student() : m_name("foo"), m_score(100) {}
Student(const string& name, int64_t score): m_name(name), m_score(score) {}
//...
};
BOOST_PYTHON_MODULE(student) {
class_<Student>("Student", "代表学生的类", init<>())
.def(init<const string&, int64_t>())
// ...
;
}__init__调用其任何一个 版本1
2
3student = Student()
student = Student("bob", 80)
student = Student("boom") # Error!
迭代器和__iter__
boost::python::iterator接受实现了begin和end方法的类作为模板,可以绑定在__iter__方法上。这样,Student实例化的对象可以支持Python里实现了迭代器__iter__的对象的各种用法,
如list(student), for x in student…
1 |
|
这里我们让Student内部维护一个m_friends数组(vector),将vector的迭代器返回。
Iterator无论是在python中还是C++中都是相当重要的概念,善用他们对于提高程序的速度和效率都很有帮助。通常我们希望在Python中调用C++代码的需求都是出于性能上的,所以有大量迭代和循环逻辑的时候,要用好这些绑定方法。
资源管理
引出的C++类型的object在其Python绑定的object被GC销毁(或手动销毁),会自动触发C++的析构函数,这点无需任何配置,为默认行为。我们在析构函数中加一条打印验证
1 | class Student { |
1 | from student import Student |
虚函数Python继承实现
C++继承
我们定义一个C++的带有虚函数的类,代表一个Increment接口的约定
1 | class IncBase { |
注意这里
inc方法为纯虚函数,无论我们在Python还是C++环境中,都必须继承并实现inc方法,否则无法直接实例化IncBase类。
在C++中继承这个Abstract class,实现inc方法
1 | class IncImp : public IncBase { |
为了展示,定义一个使用IncBase对象的函数
1 | int callInc(IncBase& i, int n) { |
将以上导出
1 | BOOST_PYTHON_MODULE(inc) { |
这里需要小心,有好几个技巧,我们逐个分析
IncBase在这里被导出,但是我们不会使用它,我们也无法使用它,作为abstract class它无法被实例化,无论在C++中还是Python中。它只作为基类的声明,如果这行,在python import时会报错extension class wrapper for base class class IncBase has not been created yet,所以为了让bases<IncBase>工作,我们把IncBase也导出boost::noncopyable:防止binding生成copy constructor,抽象类无法实例化,会提示编译错误boost::python::non_init:防止binding生成构造函数,抽象类无法实例化,会提示编译错误bases<IncBase>:需要显示指定基类bases<IncBase>,否则callInc(IncImp(), 3)会提示参数类型不匹配,因为callInc声明接受IncBase类型而不是IncImp类型
普通的Python函数是不存在任何参数类型匹配的,duck typing原则下,传入任何对象都可以,任何类型不匹配导致的错误只在访问方法时抛出。
对于函数调用,Boost Python增加的参数类型检查在默认情况下不存在所谓的runtime polymorphism,
callInc只接受IncBase对象(及其Python绑定的对象)。即使IncImp对象也有inc方法,符合duck typing,还是被认为是IncBase之外的其他类型,参数类型检查没有通过。显示声明bases<IncBase>后,我们恢复虚函数的运行时多态特性。
这是是IncBase的子类只有C++类型的情况。
Python类继承
我们希望在Python中继承抽象类并实现虚函数接口
1 | from inc import IncBase, callInc, __IncBase |
抽象类即便以Python函数的形式实现了,还是无法实例化。我们需要额外增加一个wrapper类型来使得C++侧正确调用Python子类的方法实现
1 | // omitted ... |
Python中继承wapper类型,this->get_override("inc")会调用Python子类的方法实现
1 | class MyInc(IncBase): |
XMake配置
Boost依赖的静态/动态链接
上文中我们在add_requires("boost", ...)时使用shared=False的方式静态链接boost,下面展示了如何动态链接boost,并且分发最后生成的扩展模块给python
1 | -- require boost and boost python |
多出来的部分是通过target:pkgs()遍历依赖,将dll文件拷贝到build目录,再从build目录拷贝到python模块路径上,实现python import的运行时查找链接。这跟静态链接当然互有利弊,缺点是需要分发一连串dll文件给到python用户,不过其实这不是什么大问题,用户在pip install安装我们的python模块之后,dll文件存放在用户的site-packages目录下,用户并不会感知到我们具体是采用静态链接还是动态链接