はじめに
高速化や昔のプログラムの流用などで、PythonからのC++呼び出したくなることがある。
前々から興味があったpybind11を試してみる。
この記事では公式PDF(2018.11.20時点)の4章を説明する。
環境
MacMacBook Pro (13-inch, Late 2016)
MacOS 10.12.6 Sierra
Python
pyenv 1.2.8
python 3.6.4
C++
clang-900.0.39.2
作業1: (4.3 Creating bindings for a simple function)
> pip install pybind11
でinstallできる。お手軽。
2018年11月20日の時点でinstallされるのはv2.2.4
とりあえず、説明のPDFの4章から見てみる。
#include <pybind11/pybind11.h>
int add(int i, int j) {
return i + j;
}
PYBIND11_MODULE(example, m) {
m.doc() = "pybind11 example plugin"; // optional module docstring
m.def("add", &add, "A function which adds two numbers");
}
を作って、
> 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
みたいなファイルが出来上がる。
なお、Macでやる時だけかもしれないけれど、-undefined dynamic_lookup
のオプションをつけないとerrorが出るので注意。
さて、あとはPythonで
>>> import example
>>> example.__doc__
'pybind11 example plugin'
>>> print(example.add.__doc__)
add(arg0: int, arg1: int) -> int
A function which adds two numbers
>>> example.add(1, 2)
3
>>> example.add(45, 23)
68
のようになる。
作業2: 脱線 速度計測
簡単な例で比較してみる。まずはpython側の重い和
from timeit import timeit
def sum_up(n):
sum_ = 0
for i in range(n):
sum_ += i
return sum_
if __name__ == '__main__':
n_itr = 100
n_sum = 10_000
ave_ = timeit(lambda: sum_up(n_sum), number=n_itr)/n_itr
print(f"{n_itr} loops, the average time is {ave_ * 1_000} ms.")
> python test_py.py
100 loops, the average time is 0.5563870701007545 ms.
次にC++側
#include <pybind11/pybind11.h>
int sum(int n) {
int sum_ = 0;
for(int i = 0; i < n; ++i){
sum_ += i;
}
return sum_;
}
PYBIND11_MODULE(example2, m) {
m.doc() = "pybind11 example plugin"; // optional module docstring
m.def("sum", &sum, "A function which calcurates the sum of the sequence.");
}
from timeit import timeit
import example2
if __name__ == '__main__':
n_itr = 100
n_sum = 10_000
ave_ = timeit(lambda: example2.sum(n_sum), number=n_itr)/n_itr
print(f"{n_itr} loops, the average time is {ave_ * 1_000} ms.")
> python test_cpp.py
100 loops, the average time is 0.00564772984944284 ms.
556 µs vs 5.65 µsでだいたい100倍ぐらい?
ものすごく簡単な実装だし、for loopっていうpython不利な形でこれぐらいの差。
作業3:(4.4 Keyword arguments)
keyword argumentsを実装してみる。
作業1のexample.cpp
の実装では、
>>> import example
>>> example.add(i=2, j=3)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: add(): incompatible function arguments. The following argument types are supported:
1. (arg0: int, arg1: int) -> int
Invoked with: kwargs: i=2, j=3
となってkeyword argsを受け付けてくれない。
pythonの関数は普通受け付けてくれるので、これをできるようにする。
次のように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",
py::arg("i"), py::arg("j"));
}
すると、
>>> import example
>>> example.add(i=2, j=3)
5
と受け付けるようになる。引数がどっちがどっちかわからなくなるので、引き算のsub
関数も追加する。
#include <pybind11/pybind11.h>
namespace py = pybind11;
int add(int i, int j) {
return i + j;
}
int sub(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",
py::arg("i"), py::arg("j"));
m.def("sub", &sub, "A function which subtracts the second arg from the first arg",
py::arg("i"), py::arg("j"));
}
>>> import example
>>> example.sub(5, 2)
3
>>> example.sub(i=3, j=2)
1
>>> example.sub(j=3, i=2) # jを先に入力した。
-1
ちなみに引数の名前はi
, j
にする必要はなく、
#include <pybind11/pybind11.h>
namespace py = pybind11;
int add(int i, int j) {
return i + j;
}
int sub(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",
py::arg("i"), py::arg("j"));
m.def("sub", &sub, "A function which subtracts the second arg from the first arg",
py::arg("minuend"), py::arg("subtrahend"));
}
とすれば、
>>> import example
>>> example.sub(minuend=5, subtrahend=1)
4
>>> example.sub(subtrahend=1, minuend=19)
18
とできる。
なお、
#include <pybind11/pybind11.h>
namespace py = pybind11;
int add(int i, int j) {
return i + j;
}
int sub(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",
py::arg("i"), py::arg("j"));
m.def("sub", &sub, "A function which subtracts the second arg from the first arg",
py::arg("minuend"), py::arg("subtrahend"));
using namespace pybind11::literals;
m.def("sub2", &sub, "A function which subtracts the second arg from the first arg",
"i"_a, "j"_a);
}
のexample.sub2
のように簡単な記法でkeyword argsを定義することもできる。
>>> import example
>>> example.sub2(3, 2)
1
>>> example.sub2(i=6, j=2)
4
作業4:(4.5 Default arguments)
default argumentsを実装してみる。
pythonの関数定義ではkeyword argsにdefault valueを設定することがよくある。
例えば
def func(i = 1, j = 2):
return (i + 2) * j
func()
# ==> 6
func(1, 3)
# ==> 9
func(3) # i = 3, j = 2
# ==> 10
func(j=3)
# ==> 9
という感じになる。これを。pybind11を用いて表そうとすると、次のようになる。
#include <pybind11/pybind11.h>
namespace py = pybind11;
using namespace pybind11::literals;
int func(int i, int j){
return (i + 2) * j;
}
PYBIND11_MODULE(example, m) {
m.doc() = "pybind11 example plugin";
// regular notation
m.def("func1", &func, "A function",
py::arg("i") = 1, py::arg("j") = 2);
// shorthand
m.def("func2", &func, "A function",
"i"_a = 1, "j"_a = 2);
}
arg("i") = 3
のように def
の中に記述すれば良い。
作業5 (4.6 Exporting variables)
pythonのmoduleには定数なのが設定されているものがある。それらは、attr
関数を使うと表現することができる。
#include <pybind11/pybind11.h>
namespace py = pybind11;
PYBIND11_MODULE(example, m) {
m.doc() = "pybind11 example plugin";
// integer
m.attr("val1") = 34;
// float/double
m.attr("val2") = 3.4;
// str
m.attr("val3") = "This is 340.";
// str
py::object val4 = py::cast("This is another 340.");
m.attr("val4") = val4;
}
これをコンパイルするとpythonの中で次のように呼べる。
>>> import example
>>> example.val1
34
>>> example.val2
3.4
>>> example.val3
'This is 340.'
>>> example.val4
'This is another 340.'
TODO
- 別の記事でC++側のobjectをbindする方法やnumpyやEigenをbindする方法などがPDFに書かれていたのでそれをやる。
参考資料
公式ページ(link)
公式PDF(link)
[python高速化]pybind11によるc++組み込み