Pybind实现了python对C++接口的调用,在一些处理上,C++的速度会比python更有优势,并且如果想在Python中实现对某些C++的库的调用也可以通过这种方式实现。如pybind就很好的实现了对矩阵处理库eigen的支持。
pybind11是一个开源项目,在github上你可以找到它。
下面是代码的结构。它是轻量级的库,只包含头文件,你可以在include文件夹中找到所有的正式代码,在应用的时候只要包含编译这里面的所有文件既可。其他文件包括一些工具、说明文档、一些例子和测试相关文件。
在linux下,需要安装python-dev或python3-dev,还有cmake是必须要安装的。这是因为pybind库通过python的C扩展模块的方法在python中进行C代码的调用。
安装命令:
sudo yum -y install python-devel
(在ubuntu中叫python-dev)
然后运行下面的命令。
mkdir build
cd build
cmake ..
make check -j 4
库中的CMakeLists.txt是针对tests目录下的例子编写的,运行上面的命令将编译和执行这些例子。因此这只是测试你的环境是否可以正常的编译和运行,至少应该保证能够编译通过。
最后一行用于编译所有tests中的例子,并且全部运行一遍。其实也可以用下面的命令替换这一行,而只对test中的例子进行编译(-j 4适用于多核的计算机,指明4个核并行编译,会提升总体的编译速度)。如果编译通过就可以尝试接下来的学习和应用了。
make -j 4
假如有这样一个函数,我们想在python中调用:
int add(int i, int j){ return i + j;}
为了能够更全面的了解,我们首先看一个cmake工程目录:
我们的函数放到了test.cpp中,如下所示,除了将要设置绑定的函数int add(int i, int j),你会发现多出来一部分PYBIND11_MODULE宏的调用代码。这其实是模块的入口函数,是必不可少的一部分。其中的m.doc()定义了该模块的说明。m.def()应该不难看出,定义了在python中调用的名称,接口名,以及该接口的文字说明。
test.cpp:
#include
int add(int i, int j)
{
return i + j;
}
PYBIND11_MODULE(test, m)
{
m.doc() = "pybind11 example plugin"; // optional module docstring
m.def("add", &add, "A function that adds two numbers");
}
再看目录中的Pybind11,是将整个的pybind11目录copy过来的。
CMakeLists.txt中,前面的几行都是比较熟悉的常用设置,注意最后一行,是pybind11_add_module,这是pybind自己的一个函数,跟CMake的内置add_library非常相似。第一个参数跟生成的目标文件有关系,比如在我们例子中,最终生成的test.cpython-36m-x86_64-linux-gnu.so中,第一个.前面就是直接跟这个参数相关。
后面跟的是需要编译的文件,可以包括.h和.cpp。
CMakeLists.txt:
cmake_minimum_required(VERSION 3.10)
project(c_matrix C CXX)
set(CMAKE_BUILD_TYPE "Release")
add_subdirectory(pybind11)
pybind11_add_module(test test.cpp)
上面测试时候的编译命令同样适用于这里:
mkdir buildcd buildcmake ..make -j 4
不过注意最后一行,我们用make命令,而不是make check。完成之后,在build目录下你就会看到test.cpython-36m-x86_64-linux-gnu.so,这就是编译出来的目标文件。注意:这里的test正是.so名称的第一个.之前的名称,也跟cmake文件中pybind11_add_module的第一个参数相同,还跟test.cpp中的PYBIND11_MODULE的第一个参数相同。
在该.so所在路径下的python命令行中,你可以通过下面的命令行查看其效果:
>>> import test >>> test.add(1, 2)
返回3L
当然你也可以写一个python文件,通过运行python文件来进行测试:
import testrs = test.add(1, 2)print(rs)
结果是一样的,会打印出3.
在了解了大致的创建流程后,可以通过源码查看这个宏到底做了什么,这是后续的事了。
看官方手册上的一个例子,基本就可以写出行列式相关的应用了。
#include
#include
namespace py = pybind11;
py::array_t add_arrays(py::array_t input1, py::array_t input2)
{
py::buffer_info buf1 = input1.request(), buf2 = input2.request();
if (buf1.ndim != 1 || buf2.ndim != 1)
throw std::runtime_error("Number of dimensions must be one");
if (buf1.size != buf2.size)
throw std::runtime_error("Input shapes must match"); /* No pointer is passed, so NumPy will allocate the buffer */
auto result = py::array_t(buf1.size);
py::buffer_info buf3 = result.request();
double *ptr1 = static_cast(buf1.ptr);
double *ptr2 = static_cast(buf2.ptr);
double *ptr3 = static_cast(buf3.ptr);
for (size_t idx = 0; idx < buf1.shape[0]; idx++)
ptr3[idx] = ptr1[idx] + ptr2[idx];
return result;
}
PYBIND11_MODULE(test, m)
{
m.def("add_arrays", &add_arrays, "Add two NumPy arrays");
}
有一点需要注意:
Numpy数组的存储并不能保证在内存中的连续性,并且也不能保证是按照行存储还是列存储,虽然在python中一般不会关注内存的存储方式,但是当与C交互时,这将是一个问题。
所以有必要将交互接口入参强制要求按照C语言的内存存储方式。
这时候可以通过下面的形式来强制入参格式。
void f(py::array_t
forcecast参数是默认的,可以省略。但从可读性上来讲,还是应该写上。
py::array_t add_arrays(py::array_t input1, py::array_t input2){}
尤其是在传入一个二维数组的时候,如果从一个pkl文件读出来的数据,经numpy化以后,得到的(下图中的b)就很可能是一个列存储方式的矩阵,而经过一次copy()操作(下图中的c),就可能会转化为按行存储的矩阵。
a = pd.read_pickle("a.pkl")b = a.to_numpy()c = b.copy()
如果想能够适应b和c,那么用上面的方式强制转化就很有必要了,否则得到的结果将是错误的。