C++/Python混合编程
以 C++ 为底层基础,Python 作为上层建筑,共同搭建起高性能、易维护、可扩展的混合系统。
Python 本身就有 C 接口,可以用 C 语言编写扩展模块,把一些低效耗时的功能改用 C 实现,有的时候,会把整体性能提升几倍甚至几十倍。
使用 C++ 来开发 Python 扩展。认为其中最好的一个就是pybind11。
pybind11 借鉴了“前辈”Boost.Python,能够在 C++ 和 Python 之间自由转换,任意翻译两者的语言要素,比如把 C++ 的 vector 转换为 Python 的列表,把 Python 的元组转换为 C++ 的 tuple,既可以在 C++ 里调用 Python 脚本,也可以在 Python 里调用 C++ 的函数、类。
pybind11 名字里的“11”表示它完全基于现代 C++ 开发(C++11 以上),所以没有兼容旧系统的负担。它使用了大量的现代 C++ 特性,不仅代码干净整齐,运行效率也更高。
接下来看下,如何用Pybind11,让C++辅助Python,提升Python的性能
pybind11是一个纯头文件的库,但因为必须结合Python,所以首先要有Python的开发库,然后在用pip工具安装。
pybind11 支持 Python2.7、Python3 和 PyPy,这里用的是 Python3:
apt-get install python3-dev
apt-get install python3-pip
pip3 install pybind11
pybind11 充分利用了 C++ 预处理和模板元编程,把原本无聊重复的代码都隐藏了起来,展现了“神奇的魔法”——只需要短短几行代码,就可以实现一个 Python 扩展模块。具体怎么实现呢?
只要用一个宏“PYBIND11_MODULE”,再给它两个参数,Python 模块名和 C++ 实例对象名,就可以了。
#include <pybind11/pybind11.h> // pybind11的头文件
PYBIND11_MODULE(pydemo, m) // 定义Python模块pydemo
{
m.doc() = "pybind11 demo doc"; // 模块的说明文档
} // Python模块定义结束
代码里的 pydemo
就是 Python 里的模块名,之后在 Python 脚本里必须用这个名字才能 import。
第二个参数“m”
其实是 pybind11::module
的一个实例对象,封装了所有的操作,比如这里的 doc() 就是模块的说明文档。它只是个普通的变量,起什么名字都可以,但为了写起来方便,一般都用“m”。
假设这个 C++ 源文件名是“pybind.cpp”,现在你就可以用 g++ 把它编译成在 Python 里调用的模块了,不过编译命令比较复杂:
g++ pybind.cpp \ #编译的源文件
-std=c++11 -shared -fPIC \ #编译成动态库
`python3 -m pybind11 --includes` \ #获得包含路径
-o pydemo`python3-config --extension-suffix` #生成的动态库名字
第一行是指定编译的源文件,第二行是指定编译成动态库,
第三行调用了 Python,获得 pybind11 所在的包含路径,让 g++ 能够找得到头文件。第四行最关键,是生成的动态库名字,前面必须是源码里的模块名,而后面那部分则是 Python 要求的后缀名,否则 Python 运行时会找不到模块。
编译完后会生成一个大概这样的文件:pydemo.cpython-35m-x86_64-linux-gnu.so,现在就可以在 Python 里验证了,使用 import 导入,然后用 help 就能查看模块说明:
$ python3
>>> import pydemo
>>> help(pydemo)
刚才的代码非常简单,只是个空模块,里面什么都没有,现在来看看怎么把 C++ 的函数导入 Python
需要用的是 def() 函数,传递一个 Python 函数名和 C++ 的函数、函数对象或者是 lambda 表达式,形式上和 Python 的函数也差不多:
namespace py = pybind11; // 名字空间别名,简化代码
PYBIND11_MODULE(pydemo, m) // 定义Python模块pydemo
{
m.def("info", // 定义Python函数
[]() // 定义一个lambda表达式
{
py::print("c++ version =", __cplusplus); // pybind11自己的打印函数
py::print("gcc version =", __VERSION__);
py::print("libstdc++ =", __GLIBCXX__);
}
);
m.def("add", // 定义Python函数
[](int a, int b) // 有参数的lambda表达式
{
return a + b;
}
);
} // Python模块定义结束
在 Python 里可以验证效果:
import pydemo # 导入pybind11模块
pydemo.info() # 调用C++写的函数
x = pydemo.add(1,2) # 调用C++写的函数
pybind11 也支持函数的参数、返回值使用标准容器,会自动转换成 Python 里的 list、dict,不过你需要额外再包含一个“stl.h”的头文件。
下面的示例代码演示了 C++ 的 string、tuple 和 vector 是如何用于 Python 的:
#include <pybind11/stl.h> // 转换标准容器必须的头文件
PYBIND11_MODULE(pydemo, m) // 定义Python模块pydemo
{
m.def("use_str", // 定义Python函数
[](const string& str) // 入参是string
{
py::print(str);
return str + "!!"; // 返回string
}
);
m.def("use_tuple", // 定义Python函数
[](tuple<int, int, string> x) // 入参是tuple
{
get<0>(x)++;
get<1>(x)++;
get<2>(x)+= "??";
return x; // 返回元组
}
);
m.def("use_list", // 定义Python函数
[](const vector<int>& v) // 入参是vector
{
auto vv = v;
py::print("input :", vv);
vv.push_back(100);
return vv; // 返回列表
}
);
}
因为都是面向对象的编程语言,C++ 里的类也能够等价地转换到 Python 里面调用,这要用到一个特别的模板类 class_,注意,它有意模仿了关键字 class,后面多了一个下划线。
拿一个简单的 Point 类来举个例子:
class Point final
{
public:
Point() = default;
Point(int a);
public:
int get() const;
void set(int a);
};
使用 pybind11
,你需要在模板参数里写上这个类名,然后在构造函数里指定它在 Python 里的名字。
导出成员函数还是调用函数 def()
,但它会返回对象自身的引用,所以就可以连续调用,在一句话里导出所有接口:
py::class_<Point>(m, "Point") // 定义Python类
.def(py::init()) // 导出构造函数
.def(py::init<int>()) // 导出构造函数
.def("get", &Point::get) // 导出成员函数
.def("set", &Point::set) // 导出成员函数
;
对于一般的成员函数来说,定义的方式和普通函数一样,只是你必须加上取地址操作符“&”,把它写成函数指针的形式。而构造函数则比较特殊,必须调用 init() 函数来表示,如果有参数,还需要在 init() 函数的模板参数列表里写清楚。
小试牛刀:可参考官方文档,学习其他具体方法。