2018/12/21にPyData Osakaの梅キャンPython勉強会コラボ回でpybind11について発表しました

2018/12/21(金)にPyData Osakaの梅キャンPython勉強会コラボ回でpybind11について発表しました

ブログ記事にするのが遅くなりましたが2018/12/21に【大阪工業大学】特別回 梅キャンPython勉強会【梅田キャンパス】 でpybind11に関する発表を行いました。 下記はその際の資料になります。

In [1]:
from IPython.display import IFrame
IFrame("https://www.hiromasa.info/slide/14.slides.html", "100%", "450px")
Out[1]:

pybind11の紹介

大橋宏正(PyDataオーガナイザー)

自己紹介

  • 大橋 宏正(@wrist)
  • PyDataオーガナイザー
  • 某メーカー勤務
    • 音響信号処理屋

pybind11とは

他のライブラリとの比較

  • Python C API
    • 自分で参照カウント制御が必要
    • 学習障壁が高い
  • ctypes
    • dllがあればビルド不要で使える
    • 返り値や引数の型を厳密に設定する必要有
  • cython
    • pyxの独自記法を覚える必要
    • 一旦pyxファイルをcにコンパイル
      • 更にcファイルをコンパイル
  • Boost.python
    • Boostが必要
    • 便利だが巨大でありコンパイルに時間がかかる

インストール

In [1]:
!pip install pybind11
Requirement already satisfied: pybind11 in /Users/wrist/.pyenv/versions/miniconda3-4.3.30/envs/py36/lib/python3.6/site-packages (2.2.4)

ipybind

ipybindのインストール

  • リポジトリをクローン
  • python setup.py build
  • python setup.py install

使ってみる

  • ipybind経由でpybind11を使用
  • 最初にload_extでipybindを有効化
In [2]:
%load_ext ipybind

pybind11の基本

  • PYBIND_MODULE(モジュール名, m)
  • m.def(公開名, 関数へのポインタ, 説明)
In [3]:
%%pybind11

int add(int x, int y){
    return x + y;
}

PYBIND11_MODULE(myadd, m){
    m.def("c_add", &add, "Add two integers.");
}
In [4]:
c_add(1, 2)
Out[4]:
3
In [5]:
help(c_add)
Help on built-in function c_add in module pybind11_3e2e466:

c_add(...) method of builtins.PyCapsule instance
    c_add(arg0: int, arg1: int) -> int
    
    Add two integers.

In [6]:
# c_add("foo", "bar")
In [7]:
%%pybind11

PYBIND11_MODULE(myadd, m){
    m.def("lambda_add", [](int a, int b){ return a + b;},
         "Add two integers.");
}
In [8]:
lambda_add(1, 2)
Out[8]:
3

その他のコンパイル方法

  • 手動ビルド
  • distutils
    • setup.pyを書く
  • cmake
    • pybind11_add_module(myadd myadd.cpp)

単純なクラスをラップ

  • c++のクラスを定義
  • pybind11でラップ
    • py::classs_<C++クラス名>(m, 公開クラス名)
In [9]:
%%pybind11

namespace py = pybind11;

class Coordinate {
public:
    int x_;
    int y_;
    Coordinate(){ x_ = 0; y_ = 0; }
    Coordinate(int x, int y){ x_ = x; y_ = y; }
    float Norm(){ return sqrt(x_*x_ + y_*y_); }
};

PYBIND11_MODULE(coord, m) {
    py::class_<Coordinate>(m, "Coordinate")
        .def(py::init<>())
        .def(py::init<int, int>())
        .def_readonly("x", &Coordinate::x_)
        .def_readonly("y", &Coordinate::y_)
        .def_property_readonly("norm", &Coordinate::Norm);
}

コンストラクタの定義

  • ... .def(py::init<オーバーロード引数>())
Coordinate(){ x_ = 0; y_ = 0; }
    Coordinate(int x, int y){ x_ = x; y_ = y; }

は下記のように記載

py::class_<Coordinate>(m, "Coordinate")
        .def(py::init<>())
        .def(py::init<int, int>())

プロパティの定義

  • ... .def_readonly(公開変数名, メンバへのポインタ)
py::class_<Coordinate>(m, "Coordinate")
        .def_readonly("x", &Coordinate::x_)
        .def_readonly("y", &Coordinate::y_)

computed propetyの定義

  • ... .def_propety_readonly(公開変数名, 計算関数へのポインタ)
float Norm(){ return sqrt(x_*x_ + y_*y_); }

.def_property_readonly("norm", &Coordinate::Norm);
In [10]:
point = Coordinate(3, 4)
point
Out[10]:
<pybind11_c19ca2a.Coordinate at 0x10ba96df8>
In [11]:
print(point.x, point.y)
3 4
In [12]:
point.norm
Out[12]:
5.0

さらなる例

  • 演算子オーバーロード、python特殊メソッド
  • docstring、引数名、キーワード引数
  • *arg, **kwarg

演算子オーバーロード

  • 等値演算子
    • cppだとbool operator==(const Cls& rhs)
    • pythonだとCls.__eq__(self, other)
    • これらを対応付ける
In [13]:
int.__eq__(1, 1)
Out[13]:
True
In [14]:
%%pybind11

namespace py = pybind11;

class Coordinate2 {
public:
    int x_; int y_;
    Coordinate2(){ x_ = 0; y_ = 0; }
    Coordinate2(int x, int y){ x_ = x; y_ = y; }
    bool operator==(const Coordinate2& rhs){
        return (x_ == rhs.x_) && (y_ == rhs.y_);
    }
};

PYBIND11_MODULE(coord2, m) {
    py::class_<Coordinate2>(m, "Coordinate2")
        .def(py::init<>())
        .def(py::init<int, int>())
        .def_readonly("x", &Coordinate2::x_)
        .def_readonly("y", &Coordinate2::y_)
        .def("__eq__", &Coordinate2::operator==);
}
In [15]:
point1 = Coordinate2(3, 4)
point2 = Coordinate2(3, 4)
point3 = Coordinate2(1, 2)
In [16]:
point1 == point2
Out[16]:
True
In [17]:
point1 == point3
Out[17]:
False

演算子の代替記法

.def("__eq__", &Coordinate2::operator==);

はc++のクラスでoperator==が定義されていれば下記のようにも書ける

.def("__eq__", 
             [](const Coordinate2& self,
                const Coordinate2& other){
                 return self == other; 
             });

Python特殊メソッド

  • __repr__の定義を考える
  • 先の__eq__と同様に第一引数にselfを取るメソッドを定義
In [18]:
%%pybind11

namespace py = pybind11;

class Coordinate3 {
public:
    int x_; int y_;
    Coordinate3(){ x_ = 0; y_ = 0; }
    Coordinate3(int x, int y){ x_ = x; y_ = y; }
};

PYBIND11_MODULE(coord3, m) {
    py::class_<Coordinate3>(m, "Coordinate3")
        .def(py::init<>())
        .def(py::init<int, int>())
        .def_readonly("x", &Coordinate3::x_)
        .def_readonly("y", &Coordinate3::y_)
        .def("__repr__" , [](const Coordinate3& self){
            return std::string() + "(x, y) = (" + std::to_string(self.x_) + ", " + std::to_string(self.y_) + ")";
        });
} 
.def("__repr__", [](const Coordinate3& self){
    return std::string() + 
        "(x, y) = (" +
        std::to_string(self.x_) +
        ", " +
        std::to_string(self.y_) + ")";
});
In [19]:
point = Coordinate3(1, 2)
point
Out[19]:
(x, y) = (1, 2)

docstirng、引数名、キーワード引数

  • docstring
    • m.defの第三引数に文字列を渡す
  • 引数名
    • 第四引数以降にpy::arg("...")を渡す
  • キーワード引数
    • py::arg("...") = デフォルト値とすれば良い
In [20]:
%%pybind11

namespace py = pybind11;

class Coordinate4 {
public:
    int x_; int y_;
    Coordinate4(){ x_ = 0; y_ = 0; }
    Coordinate4(int x, int y){ x_ = x; y_ = y; }
    float DistanceTo(const Coordinate4& target){ 
        int dx = target.x_ - x_, dy = target.y_ - y_;
        return sqrt( dx*dx + dy*dy);
    }
};

PYBIND11_MODULE(coord4, m) {
    py::class_<Coordinate4>(m, "Coordinate4")
        .def(py::init<>())
        .def(py::init<int, int>())
        .def_readonly("x", &Coordinate4::x_)
        .def_readonly("y", &Coordinate4::y_)
        .def("distance_to", &Coordinate4::DistanceTo,
             "distance to target",
             py::arg("target"));
}
float DistanceTo(const Coordinate4& target){ 
        int dx = target.x_ - x_, dy = target.y_ - y_;
        return sqrt( dx*dx + dy*dy);
    }

を下記のように設定

.def("distance_to", &Coordinate4::DistanceTo,
             "distance to target",
             py::arg("target"));
In [24]:
point = Coordinate4(1, 2)
target = Coordinate4(3, 4)
point.distance_to(target)
Out[24]:
2.8284270763397217
In [40]:
point.distance_to?

*args, **kwarg

  • py::args (py::tupleのサブクラス)
  • py::kwargs (py::dictのサブクラス)
m.def("count_args", [](py::args a, py::kwargs kw) {
    py::print(a.size(), "args,", kw.size(), "kwargs");
});

その他の話題

  • C++とPython間のオブジェクトの取扱
  • 関数とコールバック
  • Numpy対応

C++とPython間のオブジェクトの取扱

  • C++とPythonのオブジェクトを混在してラッパーを定義可能
  • 3種類の方法
    • Python側でC++の型のインスタンスを引数に取る
    • C++側でPythonの型のインスタンスを引数に取る
    • C++の型とPythonの型を型変換する

Python側でC++の型のインスタンスを引数に取る

py::class_<Foo>(m, "Foo");
m.def("f1", [](const Foo& foo){ ...}

C++側でPythonの型のインスタンスを引数に取る

m.def("f2", [](py::list list){ ... }

C++の型とPythonの型を型変換する

m.def("f3", [](int x) { ... });
m.def("f4", [](const std::string& s) { ... });
m.def("f5", [](const std::vector<int>& v) { ... });

サポートされている型変換

  • スカラー値
    • integer types, float, double, bool, char
  • 文字列
    • std::string, const char *
  • タプル
    • std::pair<F, S>, std::tuple<...>
  • シーケンス
    • std::vector, std::list, std::array<T, n>
  • マップ
    • std::map<K, V>, std::unordered_map<K, V>
  • 集合
    • std::set, std::unordered_set
  • 関数
    • std::function<...>
  • Date/time
    • std::chrono::duration, std::chrono::time_point
  • Optional
    • std::optional, std::experimental::optional

関数とコールバック

  • std::functionでpythonの関数を受け取れる
In [38]:
%%pybind11

#include <pybind11/functional.h>

PYBIND11_MODULE(callback_test, m) {
    m.def("for_even",
          [](int n, std::function<void(int)> f) {
              for (int i = 0; i < n; ++i){ if (i % 2 == 0) f(i); }
          }
         );
}
In [39]:
def py_callback_func(x):
    print("called {0}".format(x))
    
for_even(10, py_callback_func)
called 0
called 2
called 4
called 6
called 8

numpy対応

  • #include <pybind11/numpy.h>
  • py::array_t<type>
    • ndarrayを受け取ることが可能
  • arr.ndim, arr.shape(n), arr.size()
  • バッファへのポインタ
    • arr.data(), arr.mutable_data()
    • インデックス指定も可能
  • 要素への直接アクセス
    • arr.unchecked(), arr.mutable_unchecked()
    • ref(i, j, k)としてアクセス可能
In [31]:
%%pybind11

#include <pybind11/numpy.h>

namespace py = pybind11;

PYBIND11_MODULE(numpy_bind, m) {
    m.def("add_2d",
          [](py::array_t<double> x, py::array_t<double> y){
              auto r = x.mutable_unchecked<2>();
              auto c = y.unchecked<2>();

            for(ssize_t i = 0; i < r.shape(0); i++){
                for(ssize_t j = 0; j < r.shape(1); j++){
                    r(i, j) += c(i, j);
                }
            }
          },  py::arg().noconvert(), py::arg().noconvert());
}
In [33]:
import numpy as np
In [34]:
x = np.array([[1.0, 2.0], [3.0, 4.0]])
y = np.array([[2.0, 3.0], [4.0, 5.0]])
add_2d(x, y)
x
Out[34]:
array([[3., 5.],
       [7., 9.]])

まとめ

  • Python向けのC拡張モジュールをC++11で作成するためのライブラリpybind11について紹介
  • ipybindを用いたJupyter Notebookからの実行方法の紹介
  • numpy連携を紹介

Comments

Comments powered by Disqus