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()函數添加類方法,下面列出了主要的方法和用途:

  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

簡介

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()函數添加類方法,下面列出了主要的方法和用途:

  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,掃描下方二維碼獲取更多精彩原創內容!
公衆號二維碼

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章