本文将介绍pybind11的一些经验
环境:ubuntu18.04 python3.7 anaconda
Pybind11
安装与简介
pybind11用于python的C++扩展,它与Cython的扩展有些许不同。实话说Cython说是用于C扩展,其实更像一个python的提速工具,它并不让你从头写一个C程序。而pybind11则不一样,它没有这些自动化操作,它是一个将C++程序转换为python可用的库的工具,所以你这次需要自己写一个CPP文件,然后使用pybind11将其编译为python的库。
为什么叫pybind11?我想是因为它支持C++11特性吧。除了可以将C++程序转为python的库以加速程序,我使用它的另一个原因则是我需要一些C++特有的库。使用pybind11就可以将C++和python结合起来,既能使用各种库,还能提速,最后还可以将程序逻辑部分侧重于python,提升易用易读性。现在很多大的python库都使用了pybind11做它们的C++扩展,如Caff2(已被吞并)等。
首先还是使用anaconda安装pybind11
conda install pybind11
还要一个C++的线代库Eigen3,后面要用:
conda install eigen
如果你没有c++环境记得还要装g++:
sudo apt-get install g++
如果你是Win环境那就是要安装VS或者用mingw,这里不详细教学了。
如果之后的程序编译有问题,大多都是include路径问题,g++用命令 -I 添加include路径一般能解决。
pybind11是一个只有头文件的库,conda的安装实际就是将头文件放在了conda/include里面,把这个路径添加进include就可以了。
简单使用
这次我们将从C++代码开始,文件名mylib.cpp:
#include <pybind11/pybind11.h>
#include <pybind11/eigen.h>
#include <pybind11/stl.h>
namespace py = pybind11;
typedef Eigen::MatrixXd Mat;
Mat myfunc(float z0a,float z0b,float scale,float s) {
int N=static_cast<int> (s*scale);
Mat p(N,N);
std::complex<float> z0(z0a,z0b);
std::complex<float> z;
int color=0;
for(size_t i = 0; i < N; i++)
{
for(size_t j = 0; j < N; j++)
{
z.real((i-0.5*N)/scale);
z.imag((j-0.5*N)/scale);
color=0;
for(size_t k = 0; k < 200; k++)
{
if (std::abs(z)>4) {
color=k;
break;
}
else
z=z*z+z0;
}
p(i,j)=color;
}
}
return p;
}
PYBIND11_MODULE(mylib, m) {
m.def("myf1", &myf1);
}
这个程序内容还是Juila Set的计算,不过变成C++版本的了,其中使用的Eigen库类似numpy,就是一个线代库,以也会经常用到它。
第一部分:
#include <pybind11/pybind11.h>
#include <pybind11/eigen.h>
#include <pybind11/stl.h>
namespace py = pybind11;
typedef Eigen::MatrixXd Mat;
我们首先include了pybind11,然后为了使用cout和eigen,我们还include了他们,但是你会发现这里不是正常的include,而是pybind11/eigen和pybind11/stl。这是因为pybind11需要对这些库进行些改动才能让你最后在python里面能够使用,比如能够cout然后在python里像print一样显示,让Eigen的数据类型能够被python接受,等等。
pybind11会自动处理比较常见的数据格式,这样你的C++函数的参数与返回值都不用特地处理,像这个程序它返回了Eigen的MatrixXd,到python那边并没有报错,还变成了numpy的矩阵,这个特性十分的方便。
然后就是一个没有什么特别的纯C++程序而已了:
Mat myfunc(float z0a,float z0b,float scale,float s) {
int N=static_cast<int> (s*scale);
Mat p(N,N);
std::complex<float> z0(z0a,z0b);
std::complex<float> z;
int color=0;
for(size_t i = 0; i < N; i++)
{
for(size_t j = 0; j < N; j++)
{
z.real((i-0.5*N)/scale);
z.imag((j-0.5*N)/scale);
color=0;
for(size_t k = 0; k < 200; k++)
{
if (std::abs(z)>4) {
color=k;
break;
}
else
z=z*z+z0;
}
p(i,j)=color;
}
}
可以看到这就是个C++代码,你不用pybind11也可以运行它。
然后是重点:
PYBIND11_MODULE(mylib, m) {
m.def("myf1", &myf1);
}
这是一个pybind11的宏,基本上写完C++程序后pybind11的处理配置全在这里了。这里展示的是如何将一个函数进行转换。重点是第一个参数要和文件名一样,这里就是mylib。
然后对于函数,就是用m.def进行定义,第一个参数是python使用时候的名称,第二个就是函数指针。
然后就可以开始编译了,我使用g++进行编译,就不动用CMAKE了。在终端输入以下命令:
g++ -O3 -Wall -shared -std=c++11 -fPIC `python3 -m pybind11 --includes` mylib.cpp -o mylib`python3-config --extension-suffix`
如果说找不到库请用-I参数加上库文件的路径,一般不会有其他问题。
然后我们就获得了.so文件,这时就可以在python里import了:
from mylib import myf1
并且可以正常使用了:
z0a=-0.8
z0b=0.156
scale=100
p=myf1(z0a,z0b,scale,1)
这里我也测试了这个程序的性能,耗时0.025秒,比Cython慢很多,不知道是本来如此还是我写的代码菜了……
除了这样一个简单的函数转换,pybind对函数还有其他的设置,如:
设置函数说明:
m.def("myf1", &myf1, "A naive and simple function")
设置函数参数的名称和默认值,没错这个不输的话,你在C++里那些变量名python不认的:
m.def("myf1",&myf1,py::arg("z0a")=0,py::arg("z0b")=0,py::arg("scale"),py::arg("s"))
pybind11的函数扩展经验就分享到这。除了函数外,更大头的其实是类的扩展,pybind11对此有大量十分细节的转换与调控方法,我(可能)会在下一篇中进行分享。谢谢各位!