用 Boost.Python 写扩展库(2 类和结构体)
在 C++ 中,类和结构体本质上是一样的,唯一的区别是,类的成员默认都是 private 的,而结构体的成员默认都是 public 的。因此这里只讲类的导出方法即可。
2.1 包装简单类
当我需要导出 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() // 用于包装只读的公有成员变量
即可。
下面我们看看如何导出更多的内容。
2.2 特殊方法和运算符重载
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 + %fi", 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
搞定!
2.3 继承
假如我写了两个 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_ 的第二个模板参数即可。
2.4 总结
包装一个类,我们写的胶水代码中主要用到了如下几个类和类模板:
-
boost::python::class_<>和 boost::python::init<>
-
class_<>用来包装类;
-
class_<>::def() 方法用来包装类的方法;
-
class_<>::def_readwrite() 方法用来包装类的成员;
-
class_<>::def_readonly() 方法用来包装类的成员,并使它在 Python 中只读;
-
init<X, Y, Z> 表示类的一个构造方法接受的三个参数,分别是 X,Y,Z 类型;
-
-
boost::python::self 和 boost::python::other<>
-
self 表示重载的运算符中,操作数之一是对象本身
-
other<T> 表示重载的运算符中,操作数之一是 T 类型的对象
-
-
boost::python::base<>, 举例:class_<X, base<Y> > 表示我们要包装的类 X 继承了 类Y。