はじめに
以下の記事でご紹介した C++ と Python をつなぐ便利な pybind11 ですが、
Python インタプリタを使用する際に制約があります。
-
-
参考pybind11でPython関数の引数にC++のインスタンスを渡す方法
AIの分野では、Pythonでアルゴリズム検討を行うケースが増えており、最近C++とPythonを繋げるためのライブラリpybind11が注目されています。本記事ではC++で定義したクラスのインスタンスを引数に、Pythonの関数を呼び出す方法を紹介します。
続きを見る
本記事では、制約内容と、それを回避する方法をいくつかご紹介いたします。
制約の内容
① 生成できるインタプリタは1つだけ
先ず、公式「Interpreter lifetime」に記載されている通り、複数インタプリタを生成することができません。
Creating two concurrent scoped_interpreter guards is a fatal error.
正確には、1 つのプロセスで 1 つのみ生成できます。
② インタプリタを生成したスレッドからのみ呼び出せる
Python および pybind11 には、GIL(Global Interpreter Lock)と呼ばれる仕組みがあります。
詳しくは公式「Global Interpreter Lock」に記載されていますが、複数スレッドから Python オブジェクトへ
安全にアクセスするための仕組みです。
The Python C API dictates that the Global Interpreter Lock (GIL) must always be held by the current thread to safely access Python objects.
ある特定の処理は他のスレッドから呼び出すことが可能ですが、呼び出せない処理もあり、
インタプリタ内部で実行される処理をいちいち考えながらコーディングするのは面倒なため、
「呼び出せない」と考えた方が無難でしょう。
内部では PyGILState_Check という関数で Lock の取得状態を確認しながら処理を進めているため、
「PyGILState_Check() failure.」のようなエラーが出たら、この制約に引っかかっていることになります。
インタプリタの持ち方
では、これらの制約を回避しつつ、C++ で汎用的に実装するにはどのようにすれば良いのでしょうか。
ここではいくつかのユースケースと実装例をご紹介いたします。
ケース① 毎回生成
インタプリタを使用する際、都度インスタンスを生成します。
多くのサンプルでも記述されているように、一番簡単な使い方となります。
|
1 2 3 4 5 6 7 |
int ExecPython() { py::scoped_interpreter guard{}; // 処理を書く } |
py::scoped_interpreter は、その名の通りスマートポインタを利用しており、
インタプリタは、guard 変数のインスタンスと同じライフサイクルをたどります。
- コンストラクタで Py_Initialize() などが呼び出されてワールド初期化
- デストラクタで Py_Finalize() が呼び出されてワールド解放
という感じです。
メリット
局所的なライフサイクルとなるため、複雑なことを考えなくてよい。
デメリット
ワールドが解放されるため、毎回 import や Python コード側の初期化処理などの実行が必要になる。
ケース② 他のクラスと合わせる
C++ で実装する場合、クラスインスタンスのライフサイクルに合わせたくなります。
この場合は以下のように記述すると、インタプリタのライフサイクルを合わせることができます。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
class PyExecutor { public: static PyExecutor* GetInstance() { static PyExecutor* instance = nullptr; if (!instance) instance = new PyExecutor(); return instance; } void Execute() { // 処理を書く } private: PyExecutor() { } py::scoped_interpreter mPyGuard; }; |
Python のインタプリタは 1 プロセスに 1 つしか利用できないため、
クラスを Singleton にして保証しています。
メリット
オブジェクト指向に沿った設計を行いやすい。
デメリット
インスタンスを生成したスレッドから Execute を呼び出す必要がある。
ケース③ インタプリタだけ外出し
クラスのインスタンスを生成するスレッドと、実際に Python インタプリタを利用するスレッドが異なる場合、
以下のようにインタプリタの生成だけ外だしにした書き方にすることもできます。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
class PyInterpreterWrap { public: static PyInterpreterWrap* GetInstance() { static PyInterpreterWrap* instance = nullptr; if (!instance) instance = new PyInterpreterWrap(); return instance; } private: PyInterpreterWrap() { } py::scoped_interpreter mPyGuard; }; class PyExecutor { public: PyExecutor() { } void Execute() { // 最初の1回だけインタプリタ生成される PyInterpreterWrap::GetInstance(); // 処理を書く } }; |
メリット
PyExecutor を生成したスレッド以外から Execute を呼び出せる。
デメリット
Execute を複数インスタンス・スレッドから呼び出せると誤解を与える。
参考
- pybind11 documentation - Embedding the interpreter
- pybind11 documentation - Miscellaneous
- Stackoverflow - Keeping Python Interpreter Alive Only During the Life of an Object Instance