简介

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

  1. Boost.Python 为了兼容大多数 C++ 标准和编译器,它使用了很多可以说是魔法的操作去解决问题而变得非常的臃肿;
  2. 目前很多编译器对 C11 已经有很好的支持,而且 C11 应用也比较广泛。
    因此,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 想直接调用它。

  1. using namespace std;
  2. int add(int arg1, int arg2) {
  3. cout<< "value of arg1 is : " << arg1 << endl;
  4. return arg1 + arg2;
  5. }

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

  1. #include "existence.h"
  2. #include <pybind11/pybind11.h>
  3. PYBIND11_MODULE(example, m) {
  4. m.doc() = "pybind11 example plugin";
  5. m.def("add", &add, "A function which adds two numbers");
  6. }

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

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

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

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

这样就好了。

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

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

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

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

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

暴露类

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

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

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

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

编译

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

  1. 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,扫描下方二维码获取更多精彩原创内容!
Python调用C++之PYBIND11简介 - 图1
https://blog.csdn.net/ZM_Yang/article/details/104502772?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522159653779019724843315450%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fblog.%2522%257D&request_id=159653779019724843315450&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2first_rank_v1~rank_blog_v1-5-104502772.pc_v1_rank_blog_v1&utm_term=ONNX+Runtime&spm=1018.2118.3001.4187