在之前的文章中提到了混合编程这个做法,也提到了c++中怎样去调用python脚本,然而,python毕竟是脚本,性能还是有限的,在一些对性能要求高的情景下面,还是需要使用c/c++来完成。那怎样做呢?众所周知,python的解析器是c写成的,而c++之所以能够调用python脚本,也是得益于这点,或者说,得益于cpython。正如上一文的例子一样,要像从c++中调用python是需要做很多的封装才能得到一个比较好用的接口,同理,如果直接使用cpython,其下场应该也是一样的,这里奉上一篇写得不错的cpython使用文章。作为以懒出名的程序员,自然不会满足于此,因此有一些好用的开源库就诞生了。
Boost.Python
boost.python库在安装好之后使用起来还是很简单的,只要在链接时增加对libboost_python.so的引用就可以了。但是boost.python的问题就在这里了,首先一点,这个库依赖于boost,为了使用这个功能而引入boost,代码有点略高了;其次,boost.python使用faber来做构建,其实faber用起来还是很简单的,但是问题在于faber的文档确实是有点不那么友好了,编译好之后的库放在哪里,怎样安装都没有找到,于是,我放弃了。不过还好,在黑暗中我找到了新的救星,那就是pybind11.
pybind11
我们直接来一段pybind11官方的说明吧,网址在这里
pybind11 is a lightweight header-only library that exposes C++ types in Python and vice versa,
mainly to create Python bindings of existing C++ code.
Its goals and syntax are similar to the excellent Boost.Python library by David Abrahams:
to minimize boilerplate code in traditional extension modules by inferring type information using compile-time introspection.
从官网的说明中看到pybind11的几个特点
- 轻量级头文件库
- 目标和语法类似于优秀的Boost.python库
- 用于为python绑定c++代码
貌似确实能够满足我们的需求,但是要怎样用呢?
如何安装pybind11
由于pybind11只有都文件,所以他的安装方式相对简单,我们用python3为例子
方法1,直接使用pip安装
方法2,源代码安装
git clone https://github.com/pybind/pybind11
cd pybind11 && mkdir build && cd build && cmake ..
sudo make install
由于pybind11依赖于pytest,所以在安装前需要先把pytest给安装上
pip3 install python3-dev pytest
不过在cmake时,有可能会提示缺失了Catch,不过没有关系,cmake的提示上会把解决方式带上的,只要执行下 cmake -DDOWNLOAD_CATCH=1 就行了。
另外一个情况不知道是不是只有我遇到,就是执行make install之后在编译时依然报找不到pybind11模块,所以我另外通过在pybind11的目录里面执行
来安装了一次。到这里,一般安装是没有问题的了。
怎样使用pybind11
pybind11的用法其实相当简单,后面我们case by case的来进行举例,同时pybind11提供了很详细的使用说明书,有问题但是我没有提到的,同学们可以自行查看。
编译需要加什么选项
pybind11只有头文件,所以只要在代码中增加
#include <pybind11/pybind11.h>
就能使用pybind11了。但是由于pybind11规定了统一的模块名字格式,所以在编译时,我们需要使用类似下面的编译命令
g++ -shared -std=c++11 -fPIC `python3 -m pybind11 --includes` 要编译的源代码 -o 模块名`python3-config --extension-suffix` -I /path/to/python3
在编译完成之后,目录中会看到类似 这样的so文件,这就说明编译没有问题了。这时,只要在模块所在的目录打开python3,然后执行import语句,就能把模块给加载进去。当然,也可以通过sys.path.append()或者PYTHONPATH来指定模块所在的位置,而不是固定在so所在的目录。
特别注意:
在使用pybind11过程中会涉及3个地方用到 模块名, 这几个是必须一直的,否则会出错。这几个地方分别是
- 编译命令行中的模块名,参见上面
- 在python中import时使用到的模块名
说完了使用方式,我们下面 case by case地学习如何用pybind11封装c++代码
pybind11封装c++代码 例子
pybind11封装c++代码用法相当简单,所有的封装代码都需要在 PYBIND11_MODULE 函数里面,具体定义如下
PYBIND11_MODULE( 模块名, 模块实例对象 ){
m.doc() = "pybind11 example"; //可选,说明这个模块是做什么的
//封装的具体操作。这些操作包括普通的函数的封装,类的访问等下面用不同例子来说明问题
}
调用普通函数
pybind11的模块实例对象提供了 def()函数,用来封装普通的函数,具体的用法为
def( "给python调用方法名", &实际操作的函数, "函数功能说明" ). //其中函数功能说明为可选
下面给一个简单的例子
#include <pybind11/pybind11.h>
int add( int i, int j ){
return i+j;
}
PYBIND11_MODULE( py2cpp, m ){
m.doc() = "pybind11 example";
m.def("add", &add, "add two number" );
}
//在python中使用 模块名.函数名 来访问
//例如本例子为 py2cpp.add(1,2)
调用类函数的成员函数
pybind11提供了访问成员函数的能力。和访问普通的函数不同,访问成员函数之前先要生成一个对象实例,因此,对任意的一个类而言,都需要有两步,第一步是包装一个实例构造方法,另外一个是成员函数的范围方式
pybind11::class_<命名空间::类名>(m, "在python中构造这个类的方法名" )
.def(pybind11<>::init()) //构造器,对应的是c++类的构造函数,如果没有这个构造函数,或者参数对不是会调用失败
.def( "python中函数名", &命名空间::类名::函数名 );
下面举几个例子。
调用命名空间外的类
#include <pybind11/pybind11.h>
class Hello
{
public:
Hello(){}
void say( const std::string s ){
std::cout << s << std::endl;
}
};
PYBIND11_MODULE( py2cpp, m ){
m.doc() = "pybind11 example";
pybind11::class_<Hello>(m, "Hello" )
.def(pybind11::init())
.def( "say", &Hello::say );
}
//python 调用方式
//1, 先通过构造器来构建实例,方法为 模块名.构造器名
//2,调用对应的方法, 模块名.方法名
//例如本例子需要如下调用
// c=py2cpp.Hello()
// c.say()
调用命名空间中的对象
#include <pybind11/pybind11.h>
namespace NS{
class World{
public:
World(){}
void say( const std::string s ){
std::cout << s << std::endl;
}
};
PYBIND11_MODULE( py2cpp, m ){
m.doc() = "pybind11 example";
pybind11::class_<NS::World>(m, "World")
.def(pybind11::init())
.def("say", &NS::World::say);
}
//本例子需要如下调用
// c=py2cpp.World()
// c.say()
带有参数的c++函数的访问
上面两个例子所访问的函数都是不带参数或者只有基本数据类型的参数的,那如果带有参数要怎样处理?如果使用stl的容器又要怎样处理
带参数的构造函数
#include <iostream>
#include <pybind11/pybind11.h>
class Test{
public:
Test( int i, int j )
:mI(i),mJ(j){
}
void Print(){
std::cout <<"i= " << mI <<" j= " <<mJ << std::endl;
}
private:
int mI;
int mJ;
};
PYBIND11_MODULE( py2cpp, m ){
m.doc() = "pybind11 example";
pybind11::class_<Test>(m, "Test" )
.def( pybind11::init< int , int >() ) //构造器的模版参数列表中需要按照构造函数的参数类型填入才能调用对应的参数
.def( "print", &Test::Print );
}
//本例子需要如下调用
// c=py2cpp.Test(1,2)
// c.print()
使用stl容器的作为参数的函数
pybind11提供了stl容器的封装类,当需要处理stl容器是,只要额外包括头文件<filepybind11/stl.h>即可。pybin11提供的自动转换包括std::vector<>/std::list<>/std::array<> 转换成 Python list ;std::set<>/std::unordered_set<> 转换成 python set ; andstd::map<>/std::unordered_map<> z转换成dict 几种。至于 std::pair<> 和 std::tuple<>的转换也在 <pybind11/pybind11.h> 头文件中提供了。下面提供一个简单的例子
#include <iostream>
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
class ContainerTest{
public:
ContainerTest(){}
void Set( std::vector<int> v ){
mv = v;
}
void Print(){
for( size_t i=0; i<mv.size(); i++ ){
std::cout << mv[i] << " " ;
}
std::cout << std::endl;
}
private:
std::vector<int> mv;
};
PYBIND11_MODULE( py2cpp, m ){
m.doc() = "pybind11 example";
pybind11::class_<ContainerTest>(m, "CT" )
.def( pybind11::init() )
.def( "set", &ContainerTest::Set )
.def( "print", &ContainerTest::Print );
}
//本例子需要如下调用
// c=py2cpp.CT()
// c.set( [1,2,3] )
// c.print()
访问struct/class的公有非成员
对于公有非成员变量的访问,pybind11提供了def_readwrite()方法来支持。具体定义如下
def_readwrite("在python中访问的变量名", &要访问的变量 );
下面给出一个例子
#include <pybind11/pybind11.h>
struct ST{
std::string str;
uint64_t i;
};
PYBIND11_MODULE( py2cpp, m ){
m.doc() = "pybind11 example";
pybind11::class_<ST>(m, "ST")
.def( pybind11::init() )
.def_readwrite("str", &ST::str )
.def_readwrite("i", &ST::i );
}
//本例子需要如下调用
// c = py2cpp.ST()
// c.str="hello"
// c.i = 123
// print(c.str)
// print(c.i)
enum
enum MarkerType { chessboard, tag };
PYBIND11_MODULE( py2cpp, m ){
m.doc() = "pybind11 example";
py::enum_<MarkerType>(m, "MarkerType")
.value("chessboard", MarkerType::chessboard)
.value("tag", MarkerType::tag)
.export_values();
}
写在最后
如果有用c++来编写python模块的需求,pybind11真的是一个宝藏,在本文我只是介绍了pybind11很小一部分功能的用法,还有很多功能是需要同学们自己去探寻的,而且,pybind11拥有很详细的文档,这比Boost.Python好上很多。这里这次奉上pybind11的官方说明,大家使用过程中有不明白的可以上去看看
About this projectpybind11.readthedocs.io