Boost.Python
是
Boost
的一个组件。而
Boost
是目前很红火的准
C++
标准库,它提供了很多组件使得人们可以用
C++
语言更方便地实现更多的功能。
Boost.Python
就是
Boost
众多组件中的一个。但它是个特例,它的目的不是单纯地增强
C++
语言的功能,而是希望利用
C++
语言来增强
Python
语言的能力。使用
Boost.Python
库,我可以方便地将
C++
语言写的程序库用于
Python
语言,可以用
Python
更好地完成更多的任务。
好吧,我承认,我忘了说很重要的一点。那就是,通过
Boost.Python
,我们不仅仅可以用
C++
来扩展
Python
,我们还可以将
Python
嵌入
C++
。其实
Python
运行环境本身就提供了一套嵌入
Python
到其它语言的
API
,不过是
C
语言写的。有了这套
API
,我们就完全可以将
Python
嵌入到
C/C++
语言程序中去。但是,由于
Python
本身是一门面向对象的、动态类型的语言,还带垃圾收集,而
C
是个面向过程的、静态类型的、不带垃圾收集的程序设计语言。因此,直接使用这套
C API
非常痛苦。
Boost.Python
用面向对象
+
模板的方法将这套
C API
重新包装了一下,我们用起来就清爽多了。不过,目前这个包装还不完善,因此可能还是需要直接使用一部分
Python C API
。等它长大了我再来介绍它。
目前
Boost.Python
特性包括:
o
支持
C++
引用和指针
o
Globally Registered Type Coercions
o
自动跨模块类型转换
o
高效的函数重载
o
C++
异常到
Python
异常的转化
o
参数默认值
o
关键字参数
o
在
C++
程序中访问
Python
语言中的对象
o
导出
C++
迭代器为
Python
迭代器
o
文档字符串
目前有多个工具可以实现跟
Boost.Python
类似的功能,如
SWIG
,
SIP
等。但是它们有很大的不同。
SWIG
和
SIP
都定义了一种接口描述语言。我需要先写一个接口描述文件,用于描述我要导出的
C++
函数和类。然后通过一个翻译器,将接口描述文件翻译成
C++
程序。最后编译连接生成的
C++
程序来生成扩展库。而
Boost.Python
用于导出
C++
函数和类的时候,我需要添加的也是
C++
的代码,这是
Boost.Python
的最大特点之一。
SWIG
比较适合用来包装
C
语言程序,最近也开始增强一些对
C++
的支持,但是到目前还不支持嵌套类等
C++
特性。
SIP
似乎除了用在包装
Qt
库之外,就没几个人用。而
Boost.Python
可能是这三者之间对
C++
支持最好的一个。不过
Boost.Python
也有缺点,就是它使用了大量模板技巧,因此当要导出的元素比较多时,编译非常慢。不过幸好作为
“
胶水
”
,我并不需要经常修改和重编译,而且如果使用预编译头的话可以进一步提高编译速度。
Boost.Python
的另外一个优点是,它的设计目标就是让
C++
程序库可以透明地导出到
Python
中去。即在完全不修改原来
C++
程序的情况下,导出给
Python
用。在这种设计理念下设计出来的
Boost.Python
比同类工具支持了给完善的
C++
特性,能够最大程度地保证不修改原
C++
程序。要知道贸然修改别人的程序,往往会带来许多难以察觉的错误。
基于以上几点,我推荐大家在需要的时候使用
Boost.Python
,而不是其它。这也是我写这篇文章的最大动力
:-)
。
这里假定你已经阅读过
Boost
的安装说明,已经会默认安装
Boost
库。
Boost.Python
是
Boost
的一个组件,可以随
Boost
的众多组件一块安装,也可以单独安装。由于
Boost
中组件太多,我通常并不需要所有的组件。只安装我需要的组件可以节省很多时间和空间。安装
Boost.Python
和安装
Boost
的步骤差不多,但是安装
Boost.Python
我还需要:
o
已经安装了
Python
运行环境
o
环境变量
PYTHON_ROOT
指向
Python
运行环境所在的目录,
o
环境变量
PYTHON_VERSION
的值为
Python
的版本号。
例如
Windows
下:
PYTHON_ROOT=C:/Python24, PYTHON_VERSION=2.4
。而
Linux
下,若我安装了
Python 2.4
,其可执行文件为
/usr/bin/python2.4
,运行库在
/usr/lib/python2.4
,则:
PYTHON_ROOT=/usr
,
PYTHON_VERSION=2.4
默认情况下,使用命令
Linux
下
bjam "-sTOOLS=gcc" install
Windows
下
bjam "-sTOOLS=vc7" install
每个组件会生成八份,分别是调试版
/
发布版、静态连接库
/
动态连接库、支持单线程
/
多线程的不同组合。对于
Boost.Python
,最多可能生成十六份,因为还可以选择使用
Python
运行库的调试版
/
发布版。这么多个版本,我未必都用得着,而且生成还很慢。其实大多数情况下,我只需要用如下命令生成一个版本:
Linux
下
bjam "-sTOOLS=gcc" "-sBUILD=release <runtime-link>dynamic <threading>multi" install
Windows
下
bjam "-sTOOLS=vc7" "-sBUILD=release <runtime-link>dynamic <threading>multi" install
即选择使用发布版、动态连接库和支持多线程的版本。默认情况下会选择使用
Python
运行库的发布版,这也够用了。编译完成后,我会得到相应的库。比如我安装的
Boost
的版本是
1.32
,则
Linux
下为
libboost_python-gcc-mt.so
,
Windows
下为
boost_python-vc7-mt-1_32.lib
和
boost_python-vc7-mt.dll
。
除了组件库,我还需要安装头文件。在
Boost
的源码包的
include
目录下有一大堆
.h
文件,全部复制就可以了。值得注意的是使用
bjam
安装的
Boost.Python
经常漏掉一些头文件,导致我使用库写程序的时候经常说
“xxx
头文件
”
找不到。我还是自己动手复制一份头文件比较安全。
在
Linux
下,安装完后还要用
root
用户运行一下
ldconfig
我们现看一个
Hello World
的例子,导出一个
C++
函数。
[HTML]a52a2a 1 [HTML]0000ff/*
[HTML]a52a2a 2 [HTML]0000ff * filename: helloworld.cpp
[HTML]a52a2a 3 [HTML]0000ff [HTML]0000ff*/
[HTML]a52a2a 4 [HTML]a020f0#include [HTML]ff00ff<cstdio>
[HTML]a52a2a 5 [HTML]a020f0#include [HTML]ff00ff<boost/python.hpp> [HTML]0000ff// 包含 Boost.Python 的头文件
[HTML]a52a2a 6
[HTML]a52a2a 7 [HTML]2e8b57void speak() [HTML]0000ff// 一个将被导出的 C++ 函数
[HTML]a52a2a 8 {
[HTML]a52a2a 9 printf([HTML]ff00ff"Hello World![HTML]6a5acd/n[HTML]ff00ff");
[HTML]a52a2a10 }
[HTML]a52a2a11
[HTML]a52a2a12 [HTML]a52a2ausing [HTML]2e8b57namespace boost::python; [HTML]0000ff// 引入名字空间
[HTML]a52a2a13
[HTML]a52a2a14 BOOST_PYTHON_MODULE(Baby) [HTML]0000ff// 胶水代码,导出一个名为“Baby”的模块
[HTML]a52a2a15 {
[HTML]a52a2a16 def([HTML]ff00ff"speak", &speak); [HTML]0000ff// 该模块中有一个函数,函数名为“speak”,入口地址为 &speak
[HTML]a52a2a17 }
[HTML]a52a2a18
现在我准备将上面的例子编译生成动态连接库,作为 Python 扩展库在 Python 中调用。
[HTML]a52a2a 1 [HTML]0000ff/*
[HTML]a52a2a 2 [HTML]0000ff * filename: helloworld.cpp
[HTML]a52a2a 3 [HTML]0000ff [HTML]0000ff*/
[HTML]a52a2a 4 [HTML]a020f0#include [HTML]ff00ff<cstdio>
[HTML]a52a2a 5 [HTML]a020f0#include [HTML]ff00ff<boost/python.hpp> [HTML]0000ff// 包含 Boost.Python 的头文件
[HTML]a52a2a 6
[HTML]a52a2a 7 [HTML]2e8b57void speak() [HTML]0000ff// 一个将被导出的 C++ 函数
[HTML]a52a2a 8 {
[HTML]a52a2a 9 printf([HTML]ff00ff"Hello World![HTML]6a5acd/n[HTML]ff00ff");
[HTML]a52a2a10 }
[HTML]a52a2a11
[HTML]a52a2a12 [HTML]a52a2ausing [HTML]2e8b57namespace boost::python; [HTML]0000ff// 引入名字空间
[HTML]a52a2a13
[HTML]a52a2a14 BOOST_PYTHON_MODULE(Baby) [HTML]0000ff// 胶水代码,导出一个名为“Baby”的模块
[HTML]a52a2a15 {
[HTML]a52a2a16 def([HTML]ff00ff"speak", &speak); [HTML]0000ff// 该模块中有一个函数,函数名为“speak”,入口地址为 &speak
[HTML]a52a2a17 }
[HTML]a52a2a18
现在我准备将上面的例子编译生成动态连接库,作为 Python 扩展库在 Python 中调用。
在
Linux
下用如下命令生成动态连接库
Baby.so
:
g++ helloworld.cpp -o Baby.so -shared -I/usr/include/python2.4 -lboost_python-gcc-mt
在
Windows
下需要先设置一些环境变量
INCLUDE
,让编译起能够找到
Boost.Python
的头文件和
Python
的头文件,还要设置环境变量
LIB
,让连接器能够找到
Boost.Python
的库文件和
Python
的库文件:
set INCLUDE=%INCLUDE%;"c:/python24/include";"c:/boost/include/boost-1_32"
set LIB=%LIB%;"c:/python24/libs";"c:/boost/lib"
然后用如下命令编译连接生成动态连接库
Baby.dll
:
cl helloworld.cpp /c /EHsc /LD
link helloworld.obj /OUT:"Baby.dll" /DLL python24.lib boost_python-vc71-mt-1_32.lib
如果使用
VC7
等
IDE
来开发,同样需要对
VC
工程做相应的调整。
打开
Python
解释器,验证我是否成功:
python
>>> import Baby
>>> Baby.speak()
Hello World!
>>>
这个例子中,我导出了一个函数
speak
到模块
Baby
中。最后生成的动态连接库就是我要的
Python
扩展模块。在
Python
中可以通过模块名
"Baby"
来引入,然后使用该模块下的函数。
提醒一下,这个例子虽然简单,但是第一次用
Boost.Python
写
Python
扩展库的人未必能够一次成功。可能因为我们安装
Python
运行环境和
Boost.Python
组件时选择的安装位置不同,也可能因为生成库的设置未必一致。就算这些都一样,我们还可能安装了其它版本。但道理都是一样的,我们需要注意的只有四点:
1.
在编译时,需要让编译器知道
Boost.Python
和
Python
的头文件所在目录;
2.
在连接时,需要让连接器知道
Boost.Python
和
Python
的库文件所在目录;
3.
在连接时,让连接器知道我们要生成的是动态连接库,并且注意动态连接库的主文件名要跟模块名一致;
4.
在运行
Python
解释器并装入
Baby
模块时,需要在当前目录或系统目录下找得到
Boost.Python
和
Baby
模块对应的动态连接库;
如果使用不同的操作系统、编译器或者
IDE
、不同版本的
Python
运行环境或
Boost.Python
库,成功运行上面的例子需要的设置可能不同,但我们只要注意保证上面四点,应该不会有什么大问题。请在能够顺利运行这个例子之后,再接着看后面的内容。
在
C++
中,类和结构体本质上是一样的,唯一的区别是,类的成员默认都是
private
的,而结构体的成员默认都是
public
的。因此这里只讲类的导出方法即可。
当我需要导出
C++
类给
Python
时,比如我需要导出的类的声明如下
1
class
Complex
2
{
3
public
:
4
double
real;
5
double
imag;
6
Complex(
double
rp,
double
ip);
7
double
GetArg()
const
;
8
};
我可以使用如下胶水代码来包装
1
class_<Complex>(
"Complex"
, init<
double
,
double
>())
2
.def_readwrite(
"real"
, &Complex::real)
3
.def_readwrite(
"imag"
, &Complex::imag)
4
.def(
"GetArg"
, &Complex::GetArg)
5
;
这段胶水代码的意思是,先构造一个临时对象,该对象的类型是 init<double, double> ( 模板类 init 的一个实例 ) ,然后用字符串 "Complex" 和 这个临时对象构造另一个临时对象,该对象的类型是 class_<Complex> ( 模板类 class_ 的一个实例 ) 。然后调用第二个临时对象的 def_readwrite 方法,该方法返回这个对象本身,因此可以接着再调用这个对象的 def_readwrite 方法和 def 方法。
下面是一个完整的例子,这个例子中,为了更容易写注释,我没有使用临时对象,而是给对象取了个名字叫
pyComplex
,这样更方便一些:
1
/*
2
filename: Complex.cpp
3
*/
4
#include
<cmath>
5
#include
<boost/python.hpp>
//
包含
Boost.Python
的头文件
6
7
class
Complex
//
复数类
8
{
9
public
:
10
double
real;
//
表示实部的成员
11
double
imag;
//
表示虚部的成员
12
13
//
构造函数,以及初始化列表
14
Complex(
double
rp,
double
ip):
15
real(rp),
//
初始化实部
16
imag(ip)
//
初始化虚部
17
{
18
}
19
20
//
获取复数的幅角
21
double
GetArg()
const
22
{
23
return
atan2(imag, real);
24
}
25
26
};
27
28
using
namespace
boost::python;
//
引入名字空间
29
30
BOOST_PYTHON_MODULE(ADT)
//
胶水代码入口,导出一个名为
“ADT”
的模块
31
{
32
//
构造一个类型为
"boost::python::class_<Complex>"
的对象
pyComplex
33
//
构造参数为字符串
"Complex"
34
//
表示要将
C++
类
Complex
导出到
Python
中去,名字也叫
"Complex"
35
class_<Complex> pyComplex(
"Complex"
, no_init);
36
37
//
导出它的构造方法,声明它的构造方法有两个
double
类型的参数
38
pyComplex.def(init<
double
,
double
>());
39
40
//
导出它的公有成员
real
,
41
//
该成员在
Complex
类中的位置是
&Complex::real
42
//
导出到
Python
中之后的名字也是
"real"
43
pyComplex.def_readwrite(
"real"
, &Complex::real);
44
45
//
导出它的公有成员
imag
,
46
//
该成员在
Complex
类中的位置是
&Complex::imag
47
//
导出到
Python
中之后的名字也是
"imag"
48
pyComplex.def_readwrite(
"imag"
, &Complex::imag);
49
50
//
导出它的成员方法
GetArg
51
//
该方法在
Complex
类中的入口是
&Complex::GetArg
52
//
导出到
Python
中之后的名字也是
"GetArg"
53
pyComplex.def(
"GetArg"
, &Complex::GetArg);
54
}
用上一章讲的步骤编译这个程序,生成动态连接库 ADT.so (Linux 下 ) 或 ADT.dll (Windows 下 ) 。然后我可以执行一段 Python 脚本来验证一下:
>>> import ADT
>>> z = ADT.Complex(-1, 1.5)
>>> print z.real, '+', str(z.imag) + 'i'
-1.0 + 1.5i
>>> z.imag = 0
>>> print z.real, '+', str(z.imag) + 'i'
-1.0 + 0.0i
>>> print z.GetArg()
3.14159265359
这样,我就导出了一个
C++
的复数类给
Python
,同时导出了该复数类的构造方法、成员方法、公有成员变量。
我还可以包装一个公有成员变量,使它在
Python
中只能读不能写。这只需要我改用
boost::python::class_<Complex>:def_readonly() //
用于包装只读的公有成员变量
即可。
下面我们看看如何导出更多的内容。
Python
的对象有一堆特殊方法,相比于一般方法,这些方法有其特别的含义。比如在
Python
语言中语句
abs(obj)
将返回调用
obj.__abs__()
方法的结果。语句
len(obj)
将返回调用
obj.__len__()
方法的结果。语句
print obj
将在屏幕上打印
obj.__repr__()
的返回值。因此,当我导出
C++
类的成员方法时,若导出的方法名为
“__XXX__”
时,我需要特别注意,这些方法在
Python
中有特别的含义,因为它们将被
Python
中特定内置函数调用的。这里
“__XXX__”
可能的形式和会调用它们的内置函数分别是
| ||||||||||||||||||||||||||||||||||||||||||||||||||||||
Table 1:
内置函数及其调用的特殊方法
| ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
例如,我可以给
Complex
类增加一些这样的特殊方法:
1
class
Complex
2
{
3
public
:
4
double
real;
5
double
imag;
6
Complex(
double
rp,
double
ip);
7
double
GetArg()
const
;
8
9
10
std::string ToString()
const
;
//
新增的转换成字符串的方法
11
operator
double
()
const
;
//
新增的类型转换操作符
12
double
abs()
const
;
//
新增的方法
13
};
我可以使用如下代码来包装
1
class_<Complex>(
"Complex"
, init<
double
,
double
>())
2
.def_readwrite(
"real"
, &Complex::real)
3
.def_readwrite(
"imag"
, &Complex::imag)
4
.def(
"GetArg"
, &Complex::GetArg)
5
.def(
"__repr__"
, &Complex::ToString)
//
转换成可读字符串
6
.def(
"__float__"
, &Complex::
operator
double
)
//
转换到为
float
7
.def(
"__abs__"
, &Complex::abs)
//
求绝对值
8
;
为了方便起见,同时为了强调这几个方法的特殊性, Boost.Python 还提供了更简洁的写法用来包装特殊方法。我可以改写上面的胶水代码为
1
class_<Complex>(
"Complex"
, init<
double
,
double
>())
2
.def_readwrite(
"real"
, &Complex::real)
3
.def_readwrite(
"imag"
, &Complex::imag)
4
.def(
"GetArg"
, &Complex::GetArg)
5
.def(
"__repr__"
, &Complex::ToString)
//
转换成可读字符串
6
.def(float_(self))
//
转换到为
float
7
.def(abs(self))
//
求绝对值
8
;
待会儿讲完操作符重载后,我会给出一个完整的例子。
其实,上面的
“__XXX__”
还可能有更多的形式,并且拥有更特殊的含义,即表示运算符重载。
C++
和
Python
都支持运算符重载,不过
Python
中的运算符重载采用了更简洁的写法。比如,在
C++
中,我要重载
Complex
类的加法运算符,我需要添加方法
Complex & Complex::operator + (Complex &L);
或者添加函数
Complex & operator + (Complex &R, Complex &L);
而在
Python
中,我只需要添加方法
class Complex:
def __add__(self, L):
# ...
如果我希望包装的
C++
类支持运算符重载的话,我可以将相应的方法包装成
Python
中对应的方法。如果我给
Complex
类添加了加法运算:
class Complex
{
...
Complex &operator + (const Complex &L) const;
...
};
我可以用如下胶水代码来包装:
class_<Complex>("Complex", init<double, double>())
.def("__add__", &Complex::operator +)
;
对于这些特殊的运算符重载方法,我还可以有更方便的写法来代替上面的胶水代码:
class_<Complex>("Complex", init<double, double>())
.def(self + self)
;
有了上面介绍的工具,现在我可以给我的
Complex
类添加一坨方法重载各种常用运算符,让它用起来更方便。完整的例子如下:
1
/*
2
filename: MoreComplex.cpp
3
*/
4
#include
<cmath>
5
#include
<cstdio>
6
#include
<string>
7
#include
<boost/python.hpp>
//
包含
Boost.Python
的头文件
8
9
//
复数类
10
class
Complex
11
{
12
public
:
13
double
real;
//
实部
14
double
imag;
//
虚部
15
16
Complex(
double
rp,
double
ip);
//
构造方法
17
Complex(
const
Complex &c);
//
拷贝构造方法
18
19
double
GetArg()
const
;
//
获取复数的幅角
20
21
std::string ToString()
const
;
//
转换成可读字符串
22
operator
double
()
const
;
//
到
double
类型的隐式转换方法
23
double
abs()
const
;
//
求模
24
25
//
复数和复数以及复数和浮点数的加法、减法和乘法
26
Complex
operator
+ (
const
Complex &L)
const
;
27
Complex
operator
- (
const
Complex &L)
const
;
28
Complex
operator
+ (
double
L)
const
;
29
Complex
operator
- (
double
L)
const
;
30
Complex
operator
* (
const
Complex &L)
const
;
31
Complex
operator
* (
double
L)
const
;
32
};
33
34
using
namespace
boost::python;
//
引入名字空间
35
36
BOOST_PYTHON_MODULE(ADT)
//
胶水代码入口,导出一个名为
“ADT”
的模块
37
{
38
//
包装
Complex
类
39
class_<Complex>(
"Complex"
, init<
double
,
double
>())
40
41
//
包装另外一个构造函数
42
.def(init<
const
Complex &>())
43
44
//
包装公有成员
45
.def_readwrite(
"real"
, &Complex::real)
46
.def_readwrite(
"imag"
, &Complex::imag)
47
48
//
包装成员方法
49
.def(
"GetArg"
, &Complex::GetArg)
50
51
//
包装特殊方法
52
.def(
"
/_/_
repr
/_/_
"
, &Complex::ToString)
53
54
//
包装运算符重载
55
.def(float_(self))
56
.def(abs(self))
57
.def(self + self)
58
.def(self + other<
double
>())
59
.def(self - self)
60
.def(self - other<
double
>())
61
.def(self * self)
62
.def(self * other<
double
>())
63
;
64
}
65
66
//
下面是
Complex
类的各个成员方法的实现
67
68
Complex::Complex(
double
rp,
double
ip):
69
real(rp), imag(ip)
70
{
71
}
72
73
Complex::Complex(
const
Complex &c):
74
real(c.real), imag(c.imag)
75
{
76
}
77
78
double
Complex::GetArg()
const
79
{
80
return
atan2(imag, real);
81
}
82
83
std::string Complex::ToString()
const
84
{
85
char
buf[
100
];
86
if
(imag ==
0
)
87
sprintf(buf,
"
%f
"
, real);
88
else
89
sprintf(buf,
"
%f
+
%f
i"
, real, imag);
90
91
return
std::string(buf);
92
}
93
94
Complex::
operator
double
()
const
95
{
96
return
abs();
97
}
98
99
double
Complex::abs()
const
100
{
101
return
sqrt(real*real + imag*imag);
102
}
103
104
Complex Complex::
operator
+ (
const
Complex &L)
const
105
{
106
return
Complex(
this
->real + L.real,
this
->imag + L.imag);
107
}
108
109
Complex Complex::
operator
- (
const
Complex &L)
const
110
{
111
return
Complex(
this
->real - L.real,
this
->imag - L.imag);
112
}
113
114
Complex Complex::
operator
+ (
double
L)
const
115
{
116
return
Complex(
this
->real + L,
this
->imag);
117
}
118
119
Complex Complex::
operator
- (
double
L)
const
120
{
121
return
Complex(
this
->real - L,
this
->imag);
122
}
123
124
Complex Complex::
operator
* (
const
Complex &L)
const
125
{
126
return
Complex(real*L.real - imag*L.imag, real*L.imag + imag*L.real);
127
}
128
129
Complex Complex::
operator
* (
double
L)
const
130
{
131
return
Complex(
this
->real * L,
this
->imag * L);
132
}
用上一章讲的步骤编译这个程序,生成动态连接库 ADT.so (Linux 下 ) 或 ADT.dll (Windows 下 ) 。然后我们写一个 Python 语言脚本来看一下我的 Complex 类工作得怎么样:
1
#!/usr/bin/env python
2
# filename: test.py
3
import
ADT
4 a = ADT.Complex(3, 4)
5
b = ADT.Complex(6, 9)
6 c = ADT.Complex(-1, 0)
7
d = ADT.Complex(c)
8
print
"
a =
", a
9
print
"
b =
", b
10
print
"
c =
", c
11
print
"
d =
", d
12
print
"
a + b =
", a + b
13
print
"
a + 1 =
", a + 1
14
print
"
a * b =
", a * b
15
print
"
a * 2 =
", a * 2
16
print
"
b - a =
", b - a
17
print
"
b - 3 =
", b - 3
18
print
"
|a + b| =
", abs(a + b)
19
print
"
float(a) =
", float(a)
20
print
"
Arg(d) =
", d.GetArg()
然后我们运行这个脚本,正确情况下我们可以看到输出
a = 3.000000 + 4.000000i
b = 6.000000 + 9.000000i
c = -1.000000
d = -1.000000
a + b = 9.000000 + 13.000000i
a + 1 = 4.000000 + 4.000000i
a * b = -18.000000 + 51.000000i
a * 2 = 6.000000 + 8.000000i
b - a = 3.000000 + 5.000000i
b - 3 = 3.000000 + 9.000000i
|a + b| = 15
float(a) = 5.0
Arg(d) = 3.14159265359
搞定!
假如我写了两个
C++
类,
class Base
{
public:
void foo();
};
class Derived
{
public:
void bar();
};
如果我们错误地用如下的胶水代码包装这两个类
,
class_<Base>("Base")
.def("foo", &Base::foo)
;
class_<Derived>("Derived")
.def("bar", &Derived::bar)
;
那么在
Python
中我们调用
Derived
的
foo
方法会导致错误。因为,
Python
并不知道
Derived
是
Base
的子类,从那里继承了一个
foo
方法。我们需要这样写:
class_<Derived, base<Base> >("Derived")
.def("bar", &Derived::bar)
;
即需要使用类模板
base<Base>
作为
class_
的第二个模板参数即可。
包装一个类,我们写的胶水代码中主要用到了如下几个类和类模板
:
1.
boost::python::class_<>
和
boost::python::init<>
o
class_<>
用来包装类;
o
class_<>::def()
方法用来包装类的方法;
o
class_<>::def_readwrite()
方法用来包装类的成员;
o
class_<>::def_readonly()
方法用来包装类的成员,并使它在
Python
中只读;
o
init<X, Y, Z>
表示类的一个构造方法接受的三个参数,分别是
X
,
Y
,
Z
类型;
2.
boost::python::self
和
boost::python::other<>
o
self
表示重载的运算符中,操作数之一是对象本身
o
other<T>
表示重载的运算符中,操作数之一是
T
类型的对象
3.
boost::python::base<>,
举例:
class_<X, base<Y> >
表示我们要包装的类
X
继承了
类
Y
。
为了解决命名冲突,
C++
引入了名字空间
(namespace)
。
Python
中更广泛地使用了名字空间这个概念。在
Python
中,一个
package
是一个名字空间,一个
module
是一个名字空间,一个类也是一个名字空间。实际上,
Python
语言中的名字空间的概念跟
C++
中作用域的概念更相似。
在
Boost.Python
中,类
boost::python::scope
用来代表
Python
中的名字空间。而一个
scope
对象在构造时,会把当前名字空间切换成为自己代表的名字空间。当这个
scope
对象析构的时候,又会恢复为原来的名字空间。当我构造了一个
scope
对象,然后再包装的所有东西,都会加入到这个
scope
对象代表的名字空间中去。在胶水代码的开头会自动构造一个代表模块的
scope
,所有后面包装的东西都在这个模块的名字空间中。
构造
scope
对象时,我若不指定,它会指向当前的名字空间。我若给它一个已经包装过的类,它会指向这个类的名字空间。
这里我演示一下如何用
scope
包装
C++
的嵌套类、枚举和名字空间。
假如我们有如下的嵌套类:
class A
{
public:
class B{};
A(int i);
};
我们可以用如下胶水代码来包装:
class_<A> A_("A", init<int>());
scope *s;
s = new scope(A_);
class_<B>("B");
delete s;
但是如果类
A
的构造方法或成员方法中包含
B
类型的参数的话,上面的方法就行不通。比如我们有这样的嵌套类:
class A
{
public:
class B{};
A(B b);
};
下面的胶水代码就是错误的:
class_<A> A_("A", init<A::B>());
scope *s;
s = new scope(A_);
class_<B>("B");
delete s;
因为在包装
A
的构造方法
A::A(B b)
之前,我们需要先包装好嵌套类
B
。我用了一个小技巧,来完成这项工作。胶水如下:
class_ A_ = class_<A>("A", no_init);
scope *s;
s = new scope(A_);
class_<A::B>("B");
delete s;
A_.def(init<A::B>);
C++
中的枚举
(enum )
在
Python
中没有直接对应的语言元素。但我们可以将枚举映射到
Python
中的一个名字空间中去。比如:
enum ComplexType{ RealNumber, PureImaginary, ComplexNumber };
可以用如下胶水代码包装:
enum_<ComplexType>("ComplexType")
.value("RealNumber", RealNumber)
.value("PureImaginary", PureImaginary)
.value("ComplexNumber", ComplexNumber)
.export_values()
;
在
Python
中,我们可以通过名字
RealNumber
和
ComplexType.RealNumbe
两种方法访问枚举
Complex
的成员。
按照
Boost.Python
的设计是不鼓励直接将
C++
中的名字空间
(namespace)
暴露到
Python
中去。比如我打算把
Complex
类放到名字空间
Math
下:
namespace Math
{
class Complex
{
...
};
};
那么
Boost.Python
推荐我们将模块名改为
Math
,我们的胶水代码得这样写:
BOOST_PYTHON_MODULE(Math) // 用 "Math" 做模块名
{
class_<Math::Complex>("Complex"); // 模板参数用 Math::Complex
}
但是,我就是不想用
C++
的名字空间名作为模块名。况且,我的
C++
程序中可能有好几个名字空间,难道一定要拆成几个模块才行?
上有政策下有对策,我们这样干:
class proxy_Math{}; // 声明一个空的类,作为 namespace Math 的代言人
BOOST_PYTHON_MODULE(ADT) // 还是用 "ADT" 做模块名
{
class_<proxy_Math> pm("Math");
scope *s = new scope(pm);
class_<Math::Complex>("Complex"); // 模板参数用 Math::Complex
delete s;
}
现在我们的
Complex
类在
Python
中就可以通过
ADT.Math.Complex
来访问啦。如果我心血来潮,还加入了更多的
namespace
,我们可以包装更多的
“
代言人
”
来达到目的。
Python
和
C++
毕竟不是一家子,它们采用了不同的设计理念和发展路线。道不同,不相为谋。要相为谋,必然有摩擦有冲突。我们事先了解了可能导致摩擦的地方,免得遇到问题了手忙脚乱。比如说,
Python
中没有静态类型,也没有非虚方法。那么,我们包装方法时,需要区分虚方法和非虚方法么?进一步,
C++
的接口类只不过是一堆纯虚方法的集合,有必要原样导出到
Python
中去吗?
虚方法?前面不是一直没有涉及到虚方法么?是的,一般情况下,我们在写胶水代码的时候不用区分虚
/
非虚方法。因为我们在包装方法的时候,我们写的胶水代码本身已经保证了虚方法可以被正确调用到。比如:
class Base
{
public:
virtual void bar();
};
class Derived
{
public:
virtual void bar();
};
BOOST_PYTHON_MODULE(foo)
{
class_<Base>("Base")
.def("bar", &Base::bar) // 指向 &Base::Bar
;
class_<Derived, base<Base> >("Derived")
.def("bar", &Derived::bar) // 指向 &Derived::Bar
;
}
注意,这里说的是一般情况下,这样对待虚方法就足够了。但也有其他需要特别处理的情况。下面一节有相关的内容。
有时候,我们采用了回调
(call back)
的方法来设计我们的
C++
程序,而且我们希望回调函数可以由
Python
实现,由
C++
调用。
更一般的情形是,我们希望从
C++
导出的类可以在
Python
中继承,类的虚方法可以在
Python
中覆写
(overwrite)
,类的纯虚方法可以在
Python
中实现,而且
Python
中定义的子类可以再传递回
C++
写的模块。
这个时候,我们的胶水代码中要添加额外的内容。例如我们现在要包装的一个模块叫
Foobar
,它内容如下:
class Foo
{
public:
virtual void bar()
{
printf("C++ bar/n");
}
};
void foo_bar(Foo &f)
{
f.bar();
};
这个例子中,类
Foo
的方法
bar()
是个虚方法。我们希望在
Python
中继承
Foo
类,如下:
from Foobar import *
class PyFoo(Foo):
def bar(self):
print "Python bar"
cppfoo = Foo()
pyfoo = PyFoo()
foo_bar(cppfoo)
foo_bar(pyfoo)
如果我们按原来的方法包装这个模块,上面的脚本会输出:
C++ bar
C++ bar
原因是我们的
foo_bar
函数并不知道传进来的对象已经是
Python
中继承过的,并不知道
bar()
方法已经有了另外一份实现。实际上我们希望上面的
Python
程序运行之后,能够输出:
C++ bar
Python bar
那么我们需要给类
Foo
添加一个包装类,检查方法
bar()
是否在
Python
中覆写过。我们要写的胶水代码是:
// 类 Foo 的包装类,帮助管理虚函数的覆写
class Foo_wrapper: public Foo, public wrapper<Foo>
{
public:
// 分发函数
void bar()
{
// 先查询在~Python 中的覆写版本
override overbar = get_override("bar");
if (overbar) // 如果有覆写版本,则调用改覆写方法
overbar();
else // 否则,调用父类的版本
Foo::bar();
}
// 方法~bar() 的默认版本
void default_bar()
{
Foo::bar();
}
};
BOOST_PYTHON_MODULE(Foobar)
{
class_<Foo_wrapper, boost::noncopytable>("Foo")
.def("bar", &Foo::bar, &Foo_wrapper::default_bar)
;
def("foo_bar", foo_bar);
}
这样我们才可以得到正确的结果。
因为到目前为止,
Python
还是一个纯动态类型的语言。没有编译时期的静态类型检查,也就没有必要定义接口类。但是
C++
是静态类型的语言,需要接口类的存在。好,
Python
不需接口类,
C++
需要接口类,那么,将
C++
的程序包装给
Python
用的时候,
C++
中的接口类怎么办?
这取决于我们的设计。我们可以选择不包装
C++
接口类,我们的
C++
扩展模块一样可以工作得很好;限制是,这个接口类的实现只能由
C++
来完成。我们也可以选择将接口类包装一下,这样我们可以在
Python
中实现这个接口,更加灵活。
接口类实际上就是一堆纯虚方法的集合。只要知道怎么包装纯虚方法即可。其实纯虚方法和虚方法的包装方式差不多,也需要定义一个包装类。我们来看一个例子先:
#include <boost/python.hpp>
// 接口类 Foo
class Foo
{
public:
virtual void bar() = 0; // 纯虚函数
};
void foo_bar(Foo &f)
{
f.bar();
};
using namespace boost::python;
// 接口类 Foo 的包装类,帮助管理纯虚函数的覆写
class Foo_wrapper: public Foo, public wrapper<Foo>
{
public:
// 分发函数
void bar()
{
// 先查询在~Python 中的覆写版本
override overbar = get_override("bar");
overbar();
}
};
BOOST_PYTHON_MODULE(Foobar)
{
class_<Foo_wrapper, boost::noncopyable>("Foo")
.def("bar", pure_virtual(&Foo::bar))
;
def("foo_bar", foo_bar);
}
然后我们在
Python
程序中可以写:
from Foobar import *
class PyFoo:
def bar(self):
print 'Implemented by Python!'
pf = PyFoo()
foo_bar(pf)
运行这个脚本,我们可以得到输出:
Implemented by Python!
未完待续