

简介
写给自己看的博客


Boost::Python是一个灵活强大的C++/Python接口库。它提供了两类基本功能,
一是将C++类和函数包装为PyObject对象,二是从PyObject对象中抽取实际的C++数据。
除此之外还提供异常传播,Iterator转化等辅助功能。使用Boost::Python不但可以方便
的对已有的C++库进行封装,更进一步的是支持C++/Python混合开发。
Boost::Python现有的文档不多,中文的更少。翻译这篇文章有以下几个目的,首先是
通过翻译加深自己的理解,其次是借此机会熟悉reStructureText排班工具的使用,最后
才是希望这篇文章对其他学习Boost::Python的同学有点参考价值。基于以上理由本文
并不完全忠实原文,而是在我认为需要的地方加入我自己的理解和需要的背景资料。
Boost::Python是一个Python和C++的接口框架。Boost::Python可以高效、
无缝的将C++类和函数暴露给Python调用,反之亦然;同时这个过程不需要使用第三方的
特殊工具。Boost::python在封装C++接口时不需要对待封装的代码进行修改,即具有
non-intrusive的特性;这一特性使得Boost::Python尤其适用于为现有C++库建立Python
接口。Boost::Python使用先进的metaprogramming技术简化了封装代码的语法,使得封装
代码在形式上像是某种接口定义语言(IDL)。
按照C/C++教学的传统,我们应该从一个“hello, world”例子开始介绍。假设已有一个
C++函数:
char const* greet()
{
return "hello, world";
}
要将该函数露给Python,只需添加封装代码如下:
#include <boost/python.hpp>
BOOST_PYTHON_MODULE(hello_ext)
{
using namespace boost::python;
def("greet", greet);
}
结束了,就这么简单!接下来我们只需要将代码编译、链接成动态链接库,然后
就能从Python调用这个函数,例如:
>>> import hello_ext
>>> print hello.greet()
hello, world
在原文中本节应该描述如何使用Boost的构建工具bjam来构建上节所述的模块,但是
在实际开发一般会使用其他的构建工具或者直接使用某种IDE。为了保证信息的通用性
,本节并不描述操作某种特定工具的具体步骤,而是说明构建过程中一般的注意事项。
构建过程中需要注意的有以下几点:
- 要正确设置编译器的include路径,保证编译器能够找到Python和Boost Python
的头文件。- 要正确设置编译器的lib路径,保证编译器能找到Python和Boost Python
的库文件。- 链接器的生成目标是动态链接库而不是可执行文件。
本节描述如何通过封装将C++类暴露给Python调用。
假设有如下C++ class/struct需要暴露给Python:
struct World
{
void set(std::string msg) { this->msg = msg; }
std::string greet() { return msg; }
std::string msg;
};
相应的Boost::Python封装代码为:
#include <boost/python.hpp>
using namespace boost::python;
BOOST_PYTHON_MODULE(hello)
{
class_<World>("World")
.def("greet", &World::greet)
.def("set", &World::set);
}
这段封装代码将World类的成员函数greet和set暴露给Python,在编译生成模块后
可以在Python中调用。例如:
>>> import hello
>>> planet = hello.World()
>>> planet.set('howdy')
>>> planet.greet()
'howdy'
在以上示例中World类没有明确指定构造函数。由于World是一个struct,根据C++
规范编译器自动生成一个默认的构造函数。Boost::Python默认自动暴露这个构造函数
,因此下列代码
>>> planet = hello.World()
能够正确的工作。多数情况下,C++类的定义中包含一个或多个明确定义的构造函数,
例如我们可以将World类的定义改成
struct World
{
World(std::string msg): msg(msg) {} // added constructor
void set(std::string msg) { this->msg = msg; }
std::string greet() { return msg; }
std::string msg;
};
此时根据C++规范World类中不会自动生成默认构造函数,之前的封装代码不能通过编译。
此时我们要在封装代码中明确的为class_<World>指定构造函数。
#include <boost/python.hpp>
using namespace boost::python;
BOOST_PYTHON_MODULE(hello)
{
class_<World>("World", init<std::string>())
.def("greet", &World::greet)
.def("set", &World::set)
;
}
上例中init<std::string>()指定了参数为std::string的构造函数
(在Python中构造函数的名字是_init_)。实际应用中的类往往有多个构造函数,
我们可以在通过在def()中包含init<...>分别指定。比如说World
类还有一个参数为(double, double)的构造函数,那么封装代码为:
class_<World>("World", init<std::string>())
.def(init<double, double>())
.def("greet", &World::greet)
.def("set", &World::set)
;
另一方面,如果我们不想暴露C++类的构造函数,可以使用no_init代替init<...>,例如:
class_<Abstract>("Abstract", no_init);
这种情况下封装代码仍然生成_init_方法,只不过调用该方法总是产生
Python运行时(RuntimeError)异常。
C++类的成员变量也能被暴露为相应的Python类属性,暴露过程分为只读和读写
两种模式。考虑以下C++类:
struct Var
{
Var(std::string name) : name(name), value() {}
std::string const name;
float value;
};
这个C++类和它的成员变量可以通过以下代码暴露给Python:
class_<Var>("Var", init<std::string>())
.def_readonly("name", &Var::name)
.def_readwrite("value", &Var::value);
然后在Python中我们可以用下列代码来访问:
>>> x = hello.Var('pi')
>>> x.value = 3.14
>>> print x.name, 'is around', x.value
pi is around 3.14
>>> x.name = 'e' # can't change name
Traceback (most recent call last):
File "<stdin>", line 1, in #
AttributeError: can#t set attribute
注意成员变量name暴露为只读属性,value暴露为读写属性,试图给只读属性赋值
会产生如写结果:
>>> x.name = 'e' # can't change name
Traceback (most recent call last):
File "<stdin>", line 1, in #
AttributeError: can#t set attribute
在C++编程中公共的类成员变量被认为是不好的编程习惯。设计良好的C++类应该将
成员变量隐藏在getter/setter成员函数之后,例如:
struct Num
{
Num();
float get() const;
void set(float value);
...
};
但是在Python编程中访问类的property是正常的。访问类的property不会破环封装性,
因为Property只是getter/setter函数的另外一种语法形式。例如Num类中的getter/setter
可以封装成如下形式:
class_<Num>("Num")
.add_property("rovalue", &Num::get)
.add_property("value", &Num::get, &Num::set);
然后再Python中以这种方式访问:
>>> x = Num()
>>> x.value = 3.14
>>> x.value, x.rovalue
(3.14, 3.14)
>>> x.rovalue = 2.17 # error!
注意,rovalue是一个只读的property,因为没有提供相应的setter函数。
在之前的例子中我们没有考虑类的多态性,也即类的继承问题。在实际应用中我们
很可能需要为一系列具有继承关系的类提供封装代码,最常见的例子是一个虚基类
和多个派生类。
考虑以下简单的继承关系:
struct Base { virtual ~Base(); };
struct Derived : Base {};
以及作用于该继承关系的函数:
void b(Base*);
void d(Derived*);
Base* factory() { return new Derived; }
从前文中知道基类Base的封装方式如下:
class_<Base>("Base")
/*...*/
;
现在我们需要向Boost::Python描述Derived和Base之间的继承关系:
class_<Derived, bases<Base> >("Derived")
/*...*/
;
根据这样的描述信息Boost::Python自动完成以下工作:
- 派生类的封装自动继承基类封装的方法。
- 如果基类具有多态性,派生类对象即使以基类指针或引用方式传入Python,
仍然可以应用于需要派生类的场合。
现在我们再来封装函数b、d、factory:
def("b", b);
def("d", d);
def("factory", factory);
注意factory函数被用于生成新的Derived对象,这种情况下我们应该用调用规则
return_value_policy<manage_new_object>来告诉Python中的封装对象拥有
新生成C++对象的所有权,Python中的封装对象销毁时自动销毁相应的C++对象。
后文中我们会对调用规则进行进一步介绍。
// Tell Python to take ownership of factory's result
def("factory", factory,
return_value_policy<manage_new_object>());
本节介绍如何通过对虚函数进行封装来实现多态性。紧接上节中的例子,我们
在Base类中增加一个虚函数:
struct Base
{
virtual ~Base() {}
virtual int f() = 0;
};
Boost::Python的一个重要设计目标是尽量减少对已有的C++代码的影响,也即是所谓
的non-intrusive性质。在理想情况下不需要对已有C++代码进行修改就能完成封装,
但是当C++类中包含虚函数,同时在Python中对其进行重载,另外还要保证在C++中调用
时正确的多态性时我们必须添加一些辅助代码。具体的说是在Base类的基础上派生一个
封装类用于处理虚函数的调用:
struct BaseWrap : Base, wrapper<Base>
{
int f()
{
return this->get_override("f")();
}
};
注意封装类BaseWrap拥有两个基类,一是待封装的类Base,二是辅助模板类
wrapper<Base>。封装可以在Python中重载的类包含很多细节,辅助类模板wrapper<>
的作用是实现这些细节,简化封装工作。
上一节描述了如何使用Boost::Python的辅助模板类wrapper<>封装纯虚函数
(不包含实现的虚函数),本节则描述如何封装非纯虚函数(包含默认实现的虚函数)。
在上节中的Base类定义的基础上,给虚函数f一个默认实现,Base类的定义变成如下形式:
struct Base
{
virtual ~Base() {}
virtual int f() { return 0; }
};
封装类代码也相应改变:
struct BaseWrap : Base, wrapper<Base>
{
int f()
{
if (override f = this->get_override("f"))
return f(); // *note*
return Base::f();
}
int default_f() { return this->Base::f(); }
};
注意BaseWrap::f定义的改变。现在我们要首先检查是否有重载的f,如果没有则调用
默认实现Base::f()。
最后,使用以下代码建立Python接口:
class_<BaseWrap, boost::noncopyable>("Base")
.def("f", &Base::f, &BaseWrap::default_f)
;
注意f的Python接口定义中同时需要Base::f和BaseWrap::default_f,Boost::Python
用这两个成员函数指针分别调用重载的实现和默认实现。
Base类在Python中的行为如下:
>>> base = Base()
>>> class Derived(Base):
... def f(self):
... return 42
...
>>> derived = Derived()
>>> base.f()
0
>>> derived.f()
42
C语言本身以Operator丰富著称,通过允许Operator重载,C++语言中的Operator功能
更加强大。Boost::Python的设计中考虑到了这一点,提供了方便的Operator封装功能。
考虑一个文件指针类FilePos,该类定义了大量的特殊Operator:
class FilePos { /*...*/ };
FilePos operator+(FilePos, int);
FilePos operator+(int, FilePos);
int operator-(FilePos, FilePos);
FilePos operator-(FilePos, int);
FilePos& operator+=(FilePos&, int);
FilePos& operator-=(FilePos&, int);
bool operator<(FilePos, FilePos);
这个C++类本身及其Operator都能够通过简单直观的语法暴露到Python中:
class_<FilePos>("FilePos")
.def(self + int()) // __add__
.def(int() + self) // __radd__
.def(self - self) // __sub__
.def(self - int()) // __sub__
.def(self += int()) // __iadd__
.def(self -= other<int>())
.def(self < self); // __lt__
上面这段代码含义十分清晰,基本不用对语法再做解释。需要注意的是其中self
指代的是FilePos对象;另外,不是所有操作数的类型都支持默认转换。在+=,-=
这类“自作用”操作符的定义中可以用other<T>()取代类型T的实例。
Python中的类包含一些特殊方法,Boost::Python支持所有的标准特殊方法。可以使用
与上例类似的语法将C++函数封装为Python类的特殊方法。例如:
class Rational
{ public: operator double() const; };
Rational pow(Rational, Rational);
Rational abs(Rational);
ostream& operator<<(ostream&,Rational);
class_<Rational>("Rational")
.def(float_(self)) // __float__
.def(pow(self, other<Rational>)) // __pow__
.def(abs(self)) // __abs__
.def(str(self)) // __str__
;