用Boost.Python 写扩展库

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 中调用。
 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:    内置函数及其调用的特殊方法
方法名
内置函数
含义
方法名
内置函数
含义
__int__
int()
转换为 int 数据
__bool__
bool()
转换为 bool 数据
__long__
long()
转换为 long 数据
__float__
float()
转换为 float 数据
__complex__
complex()
转换为 complex 数据
__str__
str()
转换为字符串数据
__abs__
abs()
求绝对值
__len__
len()
求长度
__pow__
pow()
最为底数求幂
__rpow__
rpow()
作为指数求密
__hex__
hex()
转换为十六进制字符串
__oct__
oct()
转换为八进制字符串
__repr__
repr()
转换为可打印字符串
 
 
 
 
 
 
 
 
 
 
例如,我可以给  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<>
class_<> 用来包装类;
class_<>::def() 方法用来包装类的方法;
class_<>::def_readwrite() 方法用来包装类的成员;
class_<>::def_readonly() 方法用来包装类的成员,并使它在  Python 中只读;
init<X, Y, Z> 表示类的一个构造方法接受的三个参数,分别是 X Y Z 类型;
2.         boost::python::self  boost::python::other<>
self 表示重载的运算符中,操作数之一是对象本身
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!
未完待续
 
 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值