Python调用C++之PYBIND11简介

简介

PyBind11是能够让C++和Python代码之间相互调用的轻量级头文件库。在这之前已经有了一个类似功能的库:Boost.Python。既然已经有了一个类似库,而且PyBind11的目的和语法都与Boost.Python相似,为什么还要重复造轮子?原因主要有以下亮点:

  1. Boost.Python为了兼容大多数C++标准和编译器,它使用了很多可以说是魔法的操作去解决问题而变得非常的臃肿;
  2. 目前很多编译器对C++11已经有很好的支持,而且C++11应用也比较广泛。
    因此,PyBind11应运而生,他能在抛弃Boost.Python的负担同时又具备Boost.Python的简单操作。

使用

PyBind11的主要目的是将已有的C++代码接口暴露给Python去调用。例如,ONNX Runtime --一个用于ONNX格式的神经网络模型推理的引擎,其推理的核心模块是用C++写的,但是从易用性、Python AI 方面的主导地位等方面考虑,它需要将模型推理的接口暴露给Python。在之前的文章ONNX Runtime 源码阅读:模型推理过程概览中也有提到过。其接口暴露代码在$ONNX_RUNTIME/onnxruntime/python/onnxruntime_pybind_state.cc中。
将C++暴露给Python主要有两个大方向:

  1. 将函数暴露给Python;
  2. 将类暴露给Python.

暴露函数

例如,我们已经有了一个C++函数实现了一个算法,代码存放在一个名字叫existence.h的头文件中,Python想直接调用它。

using namespace std;

int add(int arg1, int arg2) {
    cout<< "value of arg1 is : " << arg1 << endl;
    return arg1 + arg2;
}

那么只需要将PyBind11的一个头文件包含并使用宏PYBIND11_MODULE,简单几句话就能实现我们的目的:

#include "existence.h"
#include <pybind11/pybind11.h>

PYBIND11_MODULE(example, m) {
    m.doc() = "pybind11 example plugin"; // optional module docstring
    m.def("add", &add, "A function which adds two numbers");
}

在上面例子中,我们在使用宏的时候给了两个宏参数:examplem,其中example是你给你的模块其的名字,m其实是一个pybind11::module类的一个实例,这是怎么做到的呢?下次有机会在解释。现在我们知道,使用pybind11::module的方法def就能将函数暴露,或者说,在名字叫example的Python模块定义了一个函数。
当然,目前只是向模块添加了一个函数,我们还希望它表现的像直接用Python写的一个函数。什么意思呢?我们知道,在Python中定义一个函数,在指定它的参数的时候,可以是关键字参数、指定参数默认值,特别是参数多的时候,这两个功能是特别又用的。但是如果我们像上面一样,使用关键字的话,会得到以下错误:

>>> example.add(arg1=1,3)
  File "<stdin>", line 1
SyntaxError: non-keyword arg after keyword arg

为了能让参数成为关键字参数,我们这样添加方法到模块:

m.def("add", &add, "A function which adds two numbers", py::arg("arg1"), py::arg("arg2"));

这样就好了。

>>> example.add(arg1=4, arg2=5)
value of arg1 is : 4
9

同理,指定参数默认值方法为:

m.def("add", &add, "A function which adds two numbers", py::arg("arg1") = 1, py::arg("arg2") = 2);

另外,值得一提的是,pybind11::module中有三个主要的方法,我们已经看到了两个,分别是doc(),用于为模块添加注释;def(),用于给模块添加方法。另外还有的就是为模块添加属性。例如我们想为这个模块添加一个字符串属性,数姓名就叫who,保存的是模块的名字,我们可以使用attr()方法:

m.attr("who") = "I'm Bai Feifei.";
>>> import example 
>>> example.who
u"I'm Bai Feifei."
>>> 

暴露类

除了暴露C++方法给Python,另外一个作用就是暴露类。因为C++和Python都支持面向对象编程,而类是面向对象的核心。
暴露类我们在ONNX Runtime的源代码中已经看到了活生生的使用场景,与使用宏来暴露模块函数类似,PyBind11使用一个模板类pybind11::class_来暴露类。
我们来看ONNX Runtime的使用:

  py::class_<InferenceSession>(m, "InferenceSession", R"pbdoc(This is the main class used to run a model.)pbdoc")
      .def(
          "load_model", [](InferenceSession* sess, std::vector<std::string>& provider_types) {
            OrtPybindThrowIfError(sess->Load());
            InitializeSession(sess, provider_types);
          },
          R"pbdoc(Load a model saved in ONNX format.)pbdoc");

同样的,这个模板类的具体实例也是通过调用一些特殊的方法将方法、属性等添加到类中,例如它也是通过def()函数添加类方法,下面列出了主要的方法和用途:

扫描二维码关注公众号,回复: 10953432 查看本文章
  1. def():添加方法到类中;
  2. def_readwrite():添加属性变量到类中;
  3. def_readonly() :添加属性常量;
  4. def_readwrite_static():添加静态变量;
  5. def_readonly_static():添加静态常量;
    这些方法的返回都是py::class_<InferenceSession>自身,所以可以链式调用。
    使用def()添加类方法的时候,对于关键字参数和默认参数的定义,与上一节说过的添加木块函数的方法是一样的,需要注意的是以下两点:
  6. 类初始化,Python中使用__init__()去对实例进行初始化,因此需要给def传递一个特殊的方法pybind11::init()去指示如何进行类的初始化工作,如下面例子所示;
  7. 函数重载,因为Python中是没有函数重载这种说法的。解决这个问题的方法就是将重载的函数变成函数指针,例如下面这个例子,第一种方法肯定是无法编译的,要使用第二种方法:
struct Pet {
    Pet(const std::string &name, int age) : name(name), age(age) { }
    void set(int age_) { age = age_; }
    void set(const std::string &name_) { name = name_; }
    std::string name;
    int age;
};
// 方法一
py::class_<Pet>(m, "Pet")
  .def(py::init<const std::string &, int>())
  .def("set",  &Pet::set, "Set the pet's age")
  .def("set",  &Pet::set, "Set the pet's name");
// 方法二
py::class_<Pet>(m, "Pet")
  .def(py::init<const std::string &, int>())
  .def("set", (void (Pet::*)(int)) &Pet::set, "Set the pet's age")
  .def("set", (void (Pet::*)(const std::string &)) &Pet::set, "Set the pet's name");

编译

编译就简单了,给出一条命令

c++ -O3 -Wall -shared -std=c++11 -fPIC -I//home/zhou/repos/pybind11/include  py_export.cpp -I/home/zhou/venvs/common/include/python2.7 -o example.so

只需要对-I参数和文件名作修改就可以编译通过,关于编译命令的介绍,可以参看之前的文章GCC 命令简明教程。当然,对于复杂结构的代码,可以使用make工具来构建,或者使用cmake,但这已经不是本文所讨论的范畴,这里就不展开了。

总结

本文只是简单介绍了PyBind11的用法,如果需要用到一些更高级的功能或者想更深入了解PyBind11的用法,请参阅参考文档。
下一篇会深入PyBind11的源码,一探究竟。

References

https://buildmedia.readthedocs.org/media/pdf/pybind11/master/pybind11.pdf


本文首发于个人微信公众号TensorBoy。如果你觉得内容还不错,欢迎分享并关注我的微信公众号TensorBoy,扫描下方二维码获取更多精彩原创内容!
公众号二维码

发布了45 篇原创文章 · 获赞 4 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/ZM_Yang/article/details/104502772