背景
Pythonのfor文
は計算時間が長いという問題があり、そこを高速化したかったので、C++でアルゴリズムを書いて、pythonにインポートするということをしました。
その際、pybind11というパッケージを利用したのですが、まだあまり参考にできる記事が少なかったので、その際のメモを残したいと思います。
環境
OS: macOS Mojave 10.14.2
python: pyenv 3.6.5
Pybind11とは
公式github: https://github.com/pybind/pybind11
公式ドキュメント: https://pybind11.readthedocs.io/en/stable/
pybind11とは公式ドキュメントによると
pybind11 is a lightweight header-only library that exposes C++ types in Python and vice versa, mainly to create Python bindings of existing C++ code.
ということです。つまり、PythonにC++のコードを、またC++にPythonのコードを繋げることができるライブラリです。
実はこういったことを行ってくれる他の1つのライブラリもあり、その1つにBoost.Pythonというのがあるらしいです。が、自分は触ったことがないので、こちらの参考サイトのみ載せておきます。
Pybind11とBoostの大きな違いはBoostはとても大きいライブラリだということです。単純なモジュールを呼ぶためだけにBoostを使うのは非効率的ということもあり、Pybind11は作成されました。
その辺の詳しいベンチマークなどはこちらに詳しく書いてあるので、興味あればみてみてください!
インストール方法
インストール方法は何通りかあるのですが、今回自分はpip
を使ったのでそちらの方の紹介をします。
$ pip3 install pybind11
この他にもgithub
からのインストール方法もあるのですが、実行方法が異なってくる(コンパイルする際のコマンドが違うため)ので、気をつけた方がいいです!
実装例(簡易版)
背景にも書いたのですが、自分はpythonのfor文
の計算量が多いのが問題となっていたので、配列を用いたモジュールを書きました。が、公式にも英語のサイトにも中々配列の使い方が綴られておらず(自分の裁量不足ということもあるのですが...)、試行錯誤しました。
その結果、動いたコードの簡略版で実装例を紹介します。
コード
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
void print_arrays(std::vector<int> lst) {
// 配列の長さ
int length = sizeof(lst)/sizeof(lst[0]);
// 配列の各要素を出力する
for (int i=0; i < length-1; i++) {
printf("%d\n", lst[i]);
}
}
// PYBIND11_MODULE()はPythonでimportする際の関数を作成する
PYBIND11_MODULE(example, m) {
m.doc() = "Example pybind11"; // optional module docstring
m.def("print_arrays", &print_arrays, "A function that prints array elements.");
}
実行例
あとは実行するだけです!が、ここで私は躓きました。
失敗したコマンド
上は色んなサイトを調べた結果上手くいったコマンドです。
そして、実際に多くの人が行うように、私も公式ドキュメントに従って、以下のコマンドを実行をしました。
c++ -O3 -Wall -shared -std=c++11 -fPIC `python3 -m pybind11 --includes` example.cpp -o example`python3-config --extension-suffix`
その結果、バーっとエラーが吐かれ(長すぎるため省略します)、最後に
ld: symbol(s) not found for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)
と言われてしまいました。
色々調べた結果、こちらのイシューが参考になりました。
成功したコマンド
そこで、上記のサイトを参考に、以下のコマンドで成功しました。
clang++ -O3 -Wall -shared -std=c++11 -fPIC `python -m pybind11 --includes` -undefined dynamic_lookup example.cpp -o example`python3-config --extension-suffix`
その結果、同じディレクトリにexample.cpython-36m-darwin.so
という名前のファイルができたと思います。
詳しくは調べきれていないのですが、多分pyenv
の影響かと思うので、使っている人は気をつけてください!
実行結果
ここまで来ればあとは簡単です。
先ほどのコード内でPYBIND_MODULE()
という関数を用いて、pythonでインポートする際の関数を生成しました。
あとは、REPLで以下のようにすればいけます。
$ python3
>>> import example
>>> example.print_arrays([1, 2, 3, 4, 5])
1
2
3
4
5
>>>
応用
ここまでやると、本格的にライブラリを作りたくなってきました。
というのも、pybind11の公式ドキュメントにビルドシステムというセクションがあり、自分が作ったライブラリを実際にpip
でインストールできるようになるまでが書かれていたからです(Pypiに載せるところまではやらないです)。
ディレクトリ構成
ディレクトリ構成は人それぞれの好みがあると思いますが、今回は公式ドキュメントでも紹介されていたgitのプロジェクトを参考にしました。
pybind11_example/
├ src/
│ ├ addition.cpp
│ ├ subtraction.cpp
│ └ main.cpp
├ tests/
│ └ test.py
├ setup.py
└ README.md
src/内のコード
src内には任意のファイルを作って大丈夫です!
配列の各要素を足し合わせる関数を格納したファイル
#include <pybind11/stl.h>
int sum_arrays(std::vector<int> lst) {
// 配列の長さ
int length = lst.size();
int sum = 0;
// 配列の各要素を出力する
for (int i=0; i < length; i++) {
sum += lst[i];
}
return sum;
}
配列の各要素の積を返す関数を格納したファイル
#include <pybind11/stl.h>
int product_arrays(std::vector<int> lst) {
// 配列の長さ
int length = lst.size();
int pro = 1;
// 配列の各要素を出力する
for (int i=0; i < length; i++) {
pro *= lst[i];
}
return pro;
}
上記のモジュールを束ねるファイルmain.cpp
#include <pybind11/pybind11.h>
#include "addition.cpp"
#include "product.cpp"
PYBIND11_MODULE(pybind11_example, m) {
m.doc() = "Example pybind11"; // optional module docstring
m.def("sum_arrays", &sum_arrays);
m.def("product_arrays", &product_arrays);
}
ここで、今回はライブラリ名をpybind11_example
と設定しました。これは後にpipでインストールする際に同じ名前を用いるので、気をつけてください。
setup.py
あとはsetup.py
を記述するだけです。(README.mdやtest/内は省略をします)
これはpybind11の公式ドキュメントにビルドシステムをかなり参考にしました。
基本真似をして、ext_modules=[]
とsetup()
の中身を書き換えるだけです。
...
ext_modules = [
Extension(
'pybind11_example', // ライブラリ名
['src/main.cpp'], // 参照にするファイル
include_dirs=[
# Path to pybind11 headers
get_pybind_include(),
get_pybind_include(user=True)
],
language='c++'
),
]
...
...
setup(
name='pybind11_example', // ライブラリ名
version=0.1,
author='sff1019',
description='A pybind11 example',
long_description='',
ext_modules=ext_modules,
install_requires=['pybind11>=2.2'],
cmdclass={'build_ext': BuildExt},
zip_safe=False,
)
これで完成です!
実行
pybind11_example11
の親ディレクトリに入ります。
$ pip install ./pybind11_example
Processing ./pybind11_example
...
Successfully installed pybind11-example-0.1
となれば成功です。
あとは好きなところで
import pybind11_example
pybind11_example.sum_arrays([1,2,3])
のように実行ができます。
ちなみに、出力は以下のようになります。
6
最後に
最初のpyenvのところだけが突っかかりましたが、思った以上に簡単にC++のコードをpythonにインポートすることができるのだな、と感じました。