こんにちは。
現役エンジニアの”はやぶさ”@Cpp_Learningです。
仕事の都合もあり「C言語 ⇒ C++ ⇒ Python」の順にプログラミング言語を習得しました。
最近は、PythonとC/C++両方を使って仕事をしています。
Pythonでプログラミングをしていると…
【やりたいこと】
- 処理速度を向上させたい
- 過去に作ったC/C++のソースコードをPythonから呼びたい
と思うことがあります。
いくつか方法はありますが”Python C API”を使うことで【やりたいこと】を実現できそうだったので、勉強してみました。
備忘録も兼ねて本記事を書きます。
Contents
Python C APIとは
”Python C API”を使うことで、PythonモジュールをC/C++のソースコードで作成することができます。
つまり『”numpy”のような”モジュール”をC/C++で自作できる』ということです↓
1 2 3 4 |
import numpy import myModule a = myModule.hoge() |
※myModuleが自作モジュール・hogeがメソッド
”Python C API”を使うことで、PythonモジュールをC/C++で自作できる
Python C APIによる作業の流れ
「C/C++でモジュールを自作してPythonで使う」までの流れを図解で説明します↓
という流れになります。
スタートがC/C++であることを除けば、Pythonでモジュールを自作する流れと同じですね。
pipを使わず、ビルドするだけでも”myModule”をimportして使うことは可能です。ただし、インストールしてpipで管理する方が楽だと思います。
Python C APIによる自作モジュールの作成手順
C/C++でPythonモジュールを作成するには、Python C APIの使い方を習得する必要があります。
Python C APIのサンプルを5つ用意したので、このサンプルを使って『Python C APIを使ったコードの書き方』を説明していきます。
Sample名 | 引数 | 戻り値 |
py_hello | なし | なし |
py_add | int | int |
py_list_param | リスト | int |
py_param_list | int | リスト |
py_list_list | リスト | リスト |
上記 表の引数・戻り値は以下の意味をもっています。
- 引数:Pythonから受け取る”値”や”リスト”※1
- 戻り値:Pythonに返す”値”や”リスト”※2
(※1)C/C++で処理したいパラメータと考えても良い
(※2)C/C++の処理結果と考えても良い
Python C API サンプル -py_hello-
最初は”Hello World”で”Python C API(Python.h)”の使い方を掴んでもらいます。
Sample名 | 引数 | 戻り値 |
py_hello | なし | なし |
py_add | int | int |
py_list_param | リスト | int |
py_param_list | int | リスト |
py_list_list | リスト | リスト |
”最初の1歩”が大変ですが、一度理解できれば”Python C API(Python.h)”を使いこなせると思います!
なお、今回説明するソースコードは、以下の記事で紹介した”WSL”で動作確認しました。
Python call C function
最初にPythonから呼ぶC/C++関数を作成します。
『雛形』は以下の通りです。
1 2 3 4 5 6 7 |
#include <Python.h> static PyObject* Py_call_C(PyObject* self, PyObject* hoge) { // C/C++でやりたい処理 return result; } |
見慣れないコードだと思いますが、ポイントは以下の通りです。
- 相対パス/絶対パスどちらでも構わないので、Python.hをインクルード
- ”引数”と”関数”の型には”PyObject”を使う
Hello World
上記の『雛形』をベースに”Hello World”をターミナルに表示する関数を作成します。
1 2 3 4 5 6 7 |
#include "../../../include/python3.6m/Python.h" static PyObject* c_helloworld(PyObject* self, PyObject* args) { printf("Hello World\n"); return Py_None; } |
解説は以下の通りです。
注意点は以下の通りです。
The Module’s Method Table and Initialization Function
次に、作成した関数(c_helloworld)をPython からどうやって呼ぶかを定義します。
これも先に『雛形』を紹介します↓
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
// myModule definition(python's name) static PyMethodDef myMethods[] = { { "メソッド名", C関数名, 引数の指定フラグ, "関数の説明" }, { "メソッド名2", C関数名2, 引数の指定フラグ, "関数の説明" }, { NULL } }; // myModule definition struct static struct PyModuleDef myModule = { PyModuleDef_HEAD_INIT, "モジュール名", "モジュールの説明", -1, myMethods }; // Initializes myModule PyMODINIT_FUNC PyInit_myModule(void) { return PyModule_Create(&myModule); } |
これも見慣れないコードなので、最初は戸惑うと思いますが、本記事のサンプルも含め複数のコードを書くと慣れると思います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
// myModule definition(python's name) static PyMethodDef myMethods[] = { { "helloworld", c_helloworld, METH_NOARGS, "Prints Hello World" }, { NULL } }; // myModule definition struct static struct PyModuleDef myModule = { PyModuleDef_HEAD_INIT, "myModule", "Python3 C API Module(Sample 1)", -1, myMethods }; // Initializes myModule PyMODINIT_FUNC PyInit_myModule(void) { return PyModule_Create(&myModule); } |
今回は”myModule”というモジュール名にし、メソッド名は”helloworld”としました。
このモジュールは”Python C APIのSample1”で、メソッドの処理内容は“Hello World”をターミナルに表示することでした。
引数がないため”METH_NOARGS”を採用しました。
- ”メソッド名”と”C/C++関数名”を揃えなくても良い(揃えても良い)
- 複数の”C/C++関数”を呼ぶ設定も可能。ただし、最後は{NULL}で終わる
- 引数がない場合は”引数の指定フラグ”に”METH_NOARGS”を採用
- 引数がある場合は”METH_NOARGS”以外を使う
- 説明していない部分は別のサンプルでも基本的に書き換え不要な部分
Sample1 -py_hello-
最終的に完成した“py_hello.c”のコードは以下です。
ビルド&インストール
もう一度、「C/C++でモジュールを自作してPythonで使う」までの流れを確認します。
先ほどソースコードの“py_hello.c”を作成したので、次はビルド&インストールを行います。
setup.py
Pythonモジュールのビルド&インストールには”setup.py”を使います。
“setup.py”の『雛形』は以下の通りです。
1 2 3 4 |
from distutils.core import setup, Extension setup(name = 'pipの登録名', version = '1.0.0', \ ext_modules = [Extension('モジュール名', ['C/C++ソースファイル'])]) |
以下が“py_hello.c”用の”setup.py”です。
1 2 3 4 |
from distutils.core import setup, Extension setup(name = 'myModule', version = '1.0.0', \ ext_modules = [Extension('myModule', ['py_hello.c'])]) |
あまりポイントはありませんが、あえて挙げるなら以下の通りです。
- ”pipの登録名”と”モジュール名”は揃えなくても良い(揃えても良い)
- インストールするファイルの拡張子が*.cppでも良い
ビルド&インストールと動作確認
”setup.py”と”py_hello.c”を同じディレクトリに保存後、ターミナルからcdコマンドで保存先に移動し、以下のコマンドでビルド&インストールを行います。
python setup.py install
buildディレクトリが生成され、中に*.soが保存されていればビルド成功です。
続いて、以下のコマンドでpipで管理しているモジュール一覧を表示させます。
pip freeze
自作モジュール名(”myModule==1.0.0”)が表示されればインストール成功です。
以下のpythonソースコードを実行して、”Hello World”が表示できれば「Pythonから拡張モジュールを使う」動作確認完了です。
1 2 3 |
import myModule myModule.helloworld() |
一連の流れを実施したときのターミナル画面は以下の通りです。
以上が”Python C API(Python.h)”の最もシンプルな使い方です。
引数や戻り値の違いでコードの書き方は変わりますが、『雛形』は同じものを使えます。
(『雛形』ですから)
なので、以降からは「引数や戻り値の違いでコードがどう変わるのか?」を中心に説明します。
Python C API サンプル -py_add-
次は2変数の足し算”c = a + b”のサンプルを使って説明をします。
Sample名 | 引数 | 戻り値 |
py_hello | なし | なし |
py_add | int | int |
py_list_param | リスト | int |
py_param_list | int | リスト |
py_list_list | リスト | リスト |
”Hello World”のサンプルと異なり、引数・戻り値があります。
Pure C/C++ function
冒頭に書いた通り『過去に作ったC/C++のソースコードをPythonから呼びたい』ときがあります。
つまり、以下のような”C/C++関数”が作成済みなら、丸ごと使いたい!と考えます。
1 2 3 4 5 |
// Pure C/C++ function 'c_add' int c_add(int a, int b) { return a + b; } |
しかし、このままではPythonから呼ぶことができないので、”Python C API(Python.h)”を使ってラッパー関数を作成します。
Wrapped function
ラッパー関数といっても、作り方は”Hello World”のサンプルとあまり変わりません。
違いは引数・戻り値の有無だけなので、いきなりC/C++のコードを公開↓
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// #include <Python.h> #include "../../../include/python3.6m/Python.h" // Pure C/C++ function 'c_add' int c_add(int a, int b) { return a + b; } // Wrapped C/C++ function 'pyadd' static PyObject* pyadd(PyObject* self, PyObject* args) { int a, b, c; // Decide variable type (int, int) if (!PyArg_ParseTuple(args, "ii", &a, &b)){ return NULL; } c = c_add(a, b); // return c_add computed add number return Py_BuildValue("i", c); } |
説明の都合上、上記のC/C++関数(pyadd)を呼ぶPythonコードも先に公開↓
1 2 3 4 |
import addModule c = addModule.c_add(2, 3) print(c) |
PythonとC/C++のコードを見比べながらポイントを説明していきます。
上記のコードを別画面に表示した状態で以下の説明を読むことをオススメします。
ポイント❶ -Wrapping-
復習ですが、Pythonから呼ばれる関数の型には”PyOject”を使いました。
つまり、Pythonが呼ぶC/C++関数は”pyadd”になります。
しかし、Pythonのコードを見るとピュアC/C++関数”c_add”を呼んでいるように見えます。
c = addModule.c_add(2, 3)
これも復習ですが、”メソッド名”と”C/C++関数名”は揃えなくても良いです。
なので、関数”pyadd”をメソッド”c_add”と定義することで、さもPythonがピュアC/C++関数”c_add”を呼んでいるようなコードを書くことができます。
以上を踏まえ、関数(pyadd)をPython からどうやって呼ぶかを定義したコードが以下です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
// addModule definition(python's name) static PyMethodDef addMethods[] = { { "c_add", pyadd, METH_VARARGS}, { NULL } }; // addModule definition struct static struct PyModuleDef addModule = { PyModuleDef_HEAD_INIT, "addModule", "Python3 C API Module(Sample 2)", -1, addMethods }; // Initializes addModule PyMODINIT_FUNC PyInit_addModule(void) { return PyModule_Create(&addModule); } |
これで”c_add”をラッピングした関数”pyadd”を定義できました。
”Hello World”サンプルで説明していないポイントのみを書き出したものが以下です↓
- C/C++関数”pyadd”をPythonモジュールのメソッド”c_add”と定義
- メソッドの説明は省略できる
- モジュール名は”addModule”と定義
- サンプルを見比べ、モジュール名の変更により、書き換わる部分を確認
ポイント❷ -Python C APIによる引数・戻り値の扱い方-
次に引数・戻り値について説明します。
【Python側】
c = addModule.c_add(2, 3)
【C/C++側】
static PyObject* pyadd(PyObject* self, PyObject* args)
【Python側】で(2,3)の2つの数値を引き渡していますが、【C/C++側】では“args”のみで受け取ります。
つまり、ポイントは以下の通りです。
変数/リスト/文字列などの型に依存せず、かつ引数が複数の場合でも
”PtObject* args”を使う
ただし、“args”で受け取った値をC/C++で扱うには型を定義する必要があります。
そこで、PyArg_ParseTupleを使い、int型の引数として受け取るため”i”を使います。
int a, b, c;
if (!PyArg_ParseTuple(args, “ii”, &a, &b))
(※PyObject ⇒ int 変換)
戻り値の場合は、Py_BuildValueを使い、同じくint型なので”i”を使います。
return Py_BuildValue(“i”, c);
(※int ⇒ PyObject 変換)
もちろんint型以外も扱えます(一部引用)↓
また、引数が複数ある場合は”ii”とすることで、int型の引数2つを表現できます。
他にも以下のような引数の組み合わせが考えられます↓
Python C APIによる引数・戻り値の扱い方を整理するとこうなります↓
【Python C API -引数-】
PyArg_ParseTupleで『PyObject ⇒ int 』などC/C++で扱える型に変換
【Python C API -戻り値-】
Py_BuildValueで『int ⇒ PyObject』などPyObjectに変換してPythonに返す
【Python C API -引数・戻り値 共通-】
- intは”i”・文字列は”s”・リストは”O”など扱うものに応じて表記が異なるので、Python/C API Reference Manualで確認する
- “ii”で(int, int)・”is”で(int, 文字列)・”isi”で(int, 文字列, int)など扱うものに応じて表記が異なるので、Extending and Embedding the Python Interpreterで確認する
Sample2 -py_add-
最終的に完成した“py_add.c”のコードは以下です。
ビルド&インストールと動作確認
以下のsetup.pyでビルド&インストールを行います。
1 2 3 4 |
from distutils.core import setup, Extension setup(name = 'addModule', version = '1.0.0', \ ext_modules = [Extension('addModule', ['py_add.c'])]) |
一連の流れを実施したときのターミナル画面は以下の通りです。
まとめ -前編-
”Python C API”を用いて『C/C++でPythonの拡張モジュールを作成する方法』を説明しました。
Sample名 | 引数 | 戻り値 |
py_hello | なし | なし |
py_add | int | int |
py_list_param | リスト | int |
py_param_list | int | リスト |
py_list_list | リスト | リスト |
↑表に示す5つのサンプルを用いて、”Python C API”の入門チュートリアル記事に仕上げる予定でしたが…
かなりのボリュームになってしまったので、前編/後編に記事を分けることにしました。
後編では”Python C API(Python.h)”によるリストの扱う方について、残り3つのサンプルを用いて説明します。
後編もお楽しみに!