サブロウ丸

Sabrou-mal サブロウ丸

主にプログラミングと数学

C++コードで実装されたPythonパッケージの作成; pybind11

本稿ではC++コードを含むPythonパッケージの作成方法を紹介します。処理が重い部分をC++でコーディングすることで、パッケージが提供する関数の実行速度を向上させることができます。 pybind11を用いれば意外と(?)簡単に作成することができて素晴らしい。

完成形のイメージ

fibonacciというフィボナッチ数列値を求める関数をもつsequenceパッケージを作成します。

>>> import sequence
>>> sequence.fibonacci(10)
55

作成したコードはここ。

手順のアウトライン

"手順(C++コードのみからなるパッケージを作成)" だけでも良いのですが、後からPythonによるカスタマイズが行えるように、"手順(C++パッケージをPythonでラップしたパッケージを作成)" も本稿では紹介します。 やることが多そうですが、pybind11が公式で配布しているものを使うことで、実はあまりコストはありません。

手順 (C++コードのみからなるパッケージを作成)

フィボナッチ数列を求める関数fをC++で書く

フィボナッチ数列の関数fibonacciを実装します。

fibonacci.hpp

int fibonacci(int n) {
   if ( n <= 1 ) {
      return n;
   } else {
      return fibonacci(n-1) + fibonacci(n-2);
   }
}

モジュール化する関数やクラスを指定

init.cpp

#include <pybind11/pybind11.h>
#include "fibonacci.hpp"

PYBIND11_MODULE(sequence_cpp, m) {
   m.def("fibonacci", &fibonacci);
}

ここで、fibonacciという関数を持つsequence_cppという名前のモジュールを作るぞ、と宣言しています。 m.def("関数名", 関数のポインタ)が基本的な文法です。そのほかにもC++クラスをPythonから生成したり、ということも行えます。公式ドキュメト

これらのファイルは次のようなディレクトリ構成で管理することにします。 また、pybind11 のソースコードをこのディレクトリにダウンロードしておきます。 (git clone https://github.com/pybind/pybind11.git もしくは git submodule add https://github.com/pybind/pybind11.git など )

.
├── sequence
│   ├── fibonacci.hpp
│   └── init.cpp
└── pybind11

CMakeLists.txtの作成

cmakeを用いてc++プログラムをコンパイルします。CMakeLists.txtはコンパイル指示書のMakefileを作成する指示書です。pybind11公式がテンプレートを配布しているのでありがてぇ...と言いながら使います。

今回のファイル構成に合わせて書き換え。作業後のファイルは次のリンクを参照。 https://github.com/nariaki3551/pybind11-sample/blob/main/CMakeLists.txt

オリジナルのものとの差分は下記。

$ colordiff ./cmake_example/CMakeLists.txt CMakeLists.txt
2c2
< project(cmake_example)
---
> project(sequence_cpp)
4,5c4,7
< pybind11_add_module(cmake_example src/main.cpp)
---
> pybind11_add_module(sequence_cpp sequence/init.cpp)
> 
> target_include_directories(sequence_cpp PRIVATE ./sequence)
9c11
< target_compile_definitions(cmake_example
---
> target_compile_definitions(sequence_cpp

pybind11_add_moduleにPYBIND11_MODULEを指定したcppファイルを渡します。(複数ある場合は複数指定すれば良いのかな?)

CMakeLists.txtはルートディレクトリに配置

.
├── CMakeLists.txt  <--- 追加
├── sequence
│   ├── fibonacci.hpp
│   └── init.cpp
└── pybind11

コンパイルのテスト

CMakeLists.txtと同じ階層にいる状態で

mkdir build
cd build
cmake ..
make

が問題なく実行できればok.

setup.pyの作成

setup.pyを用いてパッケージ化しましょう。これも上記のテンプレートのものをほぼそのまま使用できます。作業のコードはこのリンクにあります。 https://github.com/nariaki3551/pybind11-sample/blob/main/setup.py

差分は下記です。

$ colordiff ./cmake_example/setup.py setup.py
1d0
< import os
130c129
<     name="cmake_example",
---
>     name="sequence",
132,133c131,132
<     author="Dean Moldovan",
<     author_email="dean0x7d@gmail.com",
---
>     author="Nariaki Tateiwa",
>     author_email="nariaki3551@gmail.com",
136c135
<     ext_modules=[CMakeExtension("cmake_example")],
---
>     ext_modules=[CMakeExtension("sequence.sequence_cpp")],

ここで

ext_modules=[CMakeExtension("sequence.sequence_cpp")]

c++ファイルからなるパッケージをsequenceパッケージ内にsequence_cppという名前で作成することを宣言しています。

setup.pyはルートディレクトリに配置

.
├── CMakeLists.txt
├── sequence
│   ├── fibonacci.hpp
│   └── init.cpp
├── setup.py  <--- 追加
└── pybind11

パッケージ化

python -m pip install .

でパッケージが作成され、利用可能になります。

>>> import sequence.sequence_cpp
>>> sequence.sequence_cpp.fibonacci(10)
55

(現在作成中のルートディレクトリで実行するとimportの読み込みが上手くいかないので注意(適当にtmpディレクトリとかを作ってそこで実行すると良いです))


手順 (C++パッケージをPythonでラップしたパッケージを作成)

さて、上記で作成したsequence.sequence_cppをラップしたパッケージを作成する方法を紹介します。

fibonacci.py

import sequence.sequence_cpp

def fibonacci(n: int) -> int:
    print("python::", end="")
    return sequence.sequence_cpp.fibonacci(n)

ここではsequence.sequence_cpp.fibonaccをラップして、標準出力に"python::"を追加してみました。

__init__.py

from sequence.fibonacci import fibonacci

これらをsequenceディレクトリに追加します。

.
├── CMakeLists.txt
├── sequence
│   ├── fibonacci.hpp
│   ├── __init__.py  <--- 追加
│   ├── fibonacci.py  <--- 追加
│   └── init.cpp
├── setup.py
└── pybind11

setup.pyの更新

setup.pyを用いてパッケージ化しましょう。差分はsetuptools.setupにpackagesを指定を追加したところですね。これで__init__.pyが存在するディレクトリをベースにパッケージが作成されます。

6c6
< from setuptools import Extension, setup, find_packages
---
> from setuptools import Extension, setup
140d139
<     packages=find_packages(),

パッケージ化

python -m pip install .

でパッケージが作成 & 利用可能になります。出力は

で、実際に実行してみると

>>> import sequence
>>> sequence.fibonacci(10)
python::55

でうまく動いていますねぇ。