pybind11の使い方

最終更新: 2019-01-14 17:32

pybind11はC++とPythonを連携させるためのライブラリである。以下のような特徴がある。

公式ドキュメントを元に、自分に必要そうな情報をまとめておく。

インストール

Ubuntuではcmakepython-devまたはpython3-devを事前にインストールする必要がある。
pippybind11をインストールしておくとコンパイルオプションが楽に指定できる。
この記事では以下のコマンドでpybind11をインストールしたことを前提とする。

sudo apt-get install python-dev python3-dev
sudo pip install pybind11

簡単な例

公式ドキュメントのFirst stepにある例をそのまま使う。

以下のコードをexample.cppとして保存する。

#include <pybind11/pybind11.h>

namespace py = pybind11;

int add(int i, int j) {
    return i + j;
}

PYBIND11_MODULE(example, m) {
    m.doc() = "pybind11 example plugin";

    m.def("add", &add, "A function which adds two numbers");
}

コンパイルは(pipでインストールした場合)

c++ -O3 -Wall -shared -std=c++11 -fPIC `python3 -m pybind11 --includes` example.cpp -o example`python3-config --extension-suffix

のように行う。Building manually を参照。

コンパイルできたらPythonから以下のように実行できる。

$ python
>>> import example
>>> example.add(1, 2)
3L

複雑なデータ構造

詳細はObject-oriented codeなどを参照のこと。
STLを使うときは#include <pybind11/stl.h>する必要がある。

example.cppのコードを以下に示す。

#include <pybind11/pybind11.h>
#include <pybind11/stl.h>

#include <vector>
#include <string>

namespace py = pybind11;

struct student {
    student(const std::string &_name) :name(_name) {}
    std::string name;
};

std::vector<std::string> createNameList(std::vector<student> &input) {
    std::vector<std::string> nameList;
    for(auto &s : input) {
        nameList.push_back(s.name);
    }
    return nameList;
}

PYBIND11_MODULE(fastclustering, m) {
    // student クラスのバインディング
    py::class_<student>(m, "student")
        .def(py::init<const std::string &>())  // コンストラクタの引数をテンプレート引数で与える
        .def_readwrite("name", &student::name); // Pythonからnameを読めるようにする

    // createNameList 関数のバインディング
    m.def("createNameList", &createNameList);
}

これを使うPythonコードの例を示す

import example

a = example.student("Alice")
b = example.student("Karen")

nameList = example.createNameList([a, b])
print(nameList)    # ['Alice', 'Karen']

引数について

詳細はKeyword argumentsを見よ。

上の例で

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

とすればPythonから

>>> example.add(i=1, j=2)

のような引数名を指定した呼び出しが可能になる。

引数にデフォルト値を与えたいときは

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

のようにする。C++の関数定義で与えたデフォルト値はPython側には適用されないのでバインディングの際に設定する必要があることに注意。

上の記述は

using namespace pybind11::literals;
m.def("add2", &add, "i"_a=1, "j"_a=2);

のように省略することもできる。(C++11のUser-defined literalsを使っているっぽい?)

PyPIへの登録

pybind11特有の情報に関しては https://github.com/pybind/python_example にあるサンプルプロジェクトが参考になる。

PyPIへのアップロード方法に関しては https://packaging.python.org/tutorials/packaging-projects/ が参考になる。(が、この通りやるとハマる)

ディレクトリ構成は以下のようになっている。

example_pkg/
  setup.py
  LICENSE
  README.md
  src/
      main.cpp

TestPyPIに自作パッケージをアップロードするまでの手順を紹介する。
まずはTestPyPIにRegisterしてアカウントを作成する。

必要なものをインストールする。

pip3 install --user --upgrade setuptools wheel twine

setup.py のあるディレクトリで以下を実行するとdist/以下に配布用のファイルが作成される。

python3 setup.py sdist 

公式ドキュメントではpython3 setup.py sdist bdist_wheel となっているが、これだとアップロードに失敗したので書き換えた。sdist のみを指定するとユーザー側でビルドすることになるらしい。(参考にしたページ

以下のコマンドで作成した配布用ファイルをTestPyPIにアップロードする。

twine upload --repository-url https://test.pypi.org/legacy/ dist/*

このコマンドを入力するとログインが要求される。
パッケージ名がすでに存在する場合はエラーになるらしい。

この時点で https://test.pypi.org/project/example-pkg のようにプロジェクトページが作成される。

pipでインストールできるかどうかを確認する。

pip3 install --extra-index-url https://test.pypi.org/simple/ example-pkg

公式ドキュメントではオプションが --index-url になっているが、この指定ではpybind11の依存関係が解決できないのでオプションを書き換える必要がある。(参考にしたページ

TestPyPIに正しくアップロードできるのであればPyPIにもアップロードできるだろう。

以下は更新していないのであまり参考にしないこと

PythonにC++の変数を見せる

詳細はExporting variablesを見よ。

PYBIND11_PLUGIN(example) {
    py::module m("example", "pybind11 example plugin");
    m.attr("the_answer") = 42;
    py::object world = py::cast("World");
    m.attr("what") = world;
    return m.ptr();
}

とすると

>>> import example
>>> example.the_answer
42L
>>> example.what
u'World'

のようにPythonからC++の変数を見ることができる。

Eigenとの連携

pybind11はEigenとの連携をサポートしている。ドキュメントのEigenのページを参照のこと。

Eigenを使うときは#include <pybind11/eigen.h>が必要。

tests/test_eigen.cpptests/test_eigen.pyに使用例が載っているので参考になる。

値渡し

詳細はPass-by-valueを見よ。

C++側のEigen::MatrixXdにPython側の(次元が対応する)numpy.ndarrayを渡すと値渡しになる。値渡しではコピーのコストが発生する上にndarrayの値を変更することができない。

参照渡し

詳細はPass-by-referenceを見よ。

C++側の引数がEigen::Ref<const MatrixType>のときはEigen::Mapを用いてなるべくコピーが回避される(一種の参照渡しになる?)。コピーが回避される条件は

という2つである。デフォルトではstorage compatibleではないことに注意。
また、条件が満たされないときはコピーが渡される

C++側の引数がEigen::Ref<MatrixType>constではない!)のときにはmapが可能かつnumpyのオブジェクトがwriteableの場合、C++側でのEigenオブジェクトに対する処理がそのままnumpyに反映される。

解決策

特にここはかなり省略した説明になっているので真面目に使うならStorage ordersを確認するべき。

storage compatible問題を解決する一番簡単な方法は引数の型ををEigen::Ref<MatrixType, 0, Eigen::Stride<Eigen::Dynamic, Eigen::Dynamic>>にすることである。これは長いのでpy::EigenDRef<MatrixType>というエイリアスが用意されている。

これを用いた例を示す。

example.cpp

#include <pybind11/pybind11.h>
#include <pybind11/eigen.h>
namespace py = pybind11;

void twice(py::EigenDRef<Eigen::MatrixXd> m) {
    m *= 2.0;
}

PYBIND11_PLUGIN(example) {
    py::module m("example", "pybind11 Eigen example plugin");
    m.def("twice", &twice, "A function which doubles numpy matrix");
    return m.ptr();
}

これを

c++ -shared -fPIC -std=c++11 -I /home/kivantium/include/pybind/include/ -I /home/kivantium/include/eigen `python-config --cflags --ldflags` example.cpp -o example.so

のようにコンパイルする(Eigenのディレクトリを指定する必要がある)。

Pythonから

$ python
>>> import numpy as np
>>> import example
>>> a = np.array([1, 2, 3], dtype=np.float64)
>>> example.twice(a)
>>> a
array([ 2.,  4.,  6.])
>>> 

のように実行することができる。

返り値について

詳細はReturning values to Pythonを見よ。

要点をまとめると