本日は Python の技術調査枠です。
Python で自作モジュールを作成してPythonにインストールする手順を記事にします。
今回はC言語を使って拡張モジュールを作成してみます。
前回記事の続きです。
bluebirdofoz.hatenablog.com
C言語で書いたPythonモジュールの作成
インストールを行うモジュールとして、以下のC言語プログラムを作成しました。
"Hello World"を標準出力する関数と、受け取った数値の足し算結果を返す関数、オプションで文字列を受け取る関数を持ちます。
・hello_module.c
// Python関連の構造体を利用するため、Python.h をインポート #include <Python.h> // "Hello World"を標準出力する関数 static PyObject* print_str (PyObject *self, PyObject *args) { // "Hello World"を標準出力する printf("Hello_world\n"); // 戻り値がない場合は関数の最後に Py_RETURN_NONE を記述する Py_RETURN_NONE; } // 数値の引数を受け取り足し算結果を返す関数 static PyObject* sum_arg(PyObject *self, PyObject *args){ // 数値を受け取るための変数を作成する int p_argx, p_argy; // 受け取った args を展開する今回は2つの数値(int)に分解する if(!PyArg_ParseTuple(args, "ii", &p_argx, &p_argy)) { // 引数の取得に失敗した場合はエラー終了 return NULL; } // 受け取った数値を足し算して int32 型に格納する int32_t result_val = p_argx + p_argy; // 足し算結果を返却する // 返却の際は必ず Python の型への変換を行う return PyLong_FromLong(result_val); } // 文字列の引数をオプションで受け取り標準出力する関数 static PyObject* print_arg(PyObject *self, PyObject *args){ // 文字列を受け取るための変数を作成する // 引数はオプションとし、引数がない場合はデフォルト値を表示する const char *p_argstr = "Default"; // 受け取った args を展開する今回は文字列(char[])に分解する // '|'以降のフォーマットはオプションとして扱われる if(!PyArg_ParseTuple(args, "|s", &p_argstr)) { // 引数の取得に失敗した場合はエラー終了 return NULL; } // 受け取った文字列を標準出力する printf(p_argstr); printf("\n"); // 出力結果を文字列としても返却する // 返却の際は必ず Python の型への変換を行う return PyUnicode_FromString(p_argstr); } // 外部から参照できるメソッドの定義 static PyMethodDef HelloextMethods[] = { // 以下のフォーマットで Python から呼び出すことのできるメソッドを定義する // {(参照時の関数名), (C言語内の関数名), (引数の定義), (関数の説明文)} // 引数を受け取らない場合は引数の定義に METH_NOARGS を設定する {"printstrext", (PyCFunction)print_str, METH_NOARGS, "print_str description"}, {"sumargext", (PyCFunction)sum_arg, METH_VARARGS, "sum_arg description"}, {"printarg", (PyCFunction)print_arg, METH_VARARGS, "print_arg description"}, // 終端を示す {NULL, NULL, 0, NULL} }; //モジュールの定義 static struct PyModuleDef helloextmodule = { PyModuleDef_HEAD_INIT, // 固定値(常に PyModuleDef_HEAD_INIT で初期化する) "helloext", // モジュールの名前 "helloext description", // モジュールの説明文 -1, // メモリ確保の指定(-1は静的な領域を確保しない) HelloextMethods // メソッドの定義への参照 }; // モジュールの初期化関数 PyMODINIT_FUNC PyInit_helloext (void) { // 構造体の初期化やモジュールの作成を行う // モジュールの作成 return PyModule_Create(&helloextmodule); }
上記のモジュールを登録する際は以下のような setup.py を用意します。
・setup.py
from distutils.core import setup, Extension setup( name="helloext", # モジュールの名前 version="1.0.0", # モジュールのバージョン author="holomon", # モジュールの作者 url="", # モジュールのホームページ description="description", # モジュールの説明(help(hello)で確認可能) install_requires=[], # 依存パッケージ([pip install -e .]実行時に一緒にインストール) extras_require={ "develop" : [], # 拡張依存パッケージ([pip install -e . develop]実行時に一緒にインストール) }, ext_modules=[ # パッケージのソースコード Extension('helloext', ['hello_module.c']) ] )
C言語で拡張モジュールを作成する場合、Extension に記述された helloext がモジュールの名前となります。
モジュールの関数について
関数の戻り値は PyObject* 、引数は PyObject *self, PyObject *args である必要があります。
static PyObject* print_str (PyObject *self, PyObject *args)
受け取った引数は PyArg_ParseTuple でパースを行います。
PyArg_ParseTuple(args, "ii", &p_argx, &p_argy)
パースを行う際は第二引数で、受け取る引数のフォーマットを指定します。
フォーマット指定の識別子は以下のページに詳細な説明があります。
docs.python.org
以下に幾つかの凡例があります。
docs.daemon.ac
戻り値は必ず Python のオブジェクト型で返却を行います。
docs.python.org
例えば、数値は以下の整数型オブジェクトに変換して返却します。
docs.python.org
インストールの実行
作成した setup.py と hello_module.c を同じディレクトリに配置します。
今回はテストのため、インストールを行う Python を直接指定します。
例えば以下のパスの Python.exe を利用して setup.py を実行すれば、実行した Python ディレクトリにモジュールがインストールされます。
D:\Python\SetupPythonTest\Python37\python.exe
setup.py を配置したディレクトリで、Shiftキー+右クリックから[PowerShellウィンドウをここで開く]を実行します。
PowerShell が起動したら、以下のコマンドを実行して hello_module.c をビルドします。
python .\setup.py build
ビルドが成功すると build ディレクトリが生成され、helloext.(ビルド環境).pyd ファイルが出力されます。
続けて、以下のコマンドを実行するとモジュールのインストールが実行されます。
python .\setup.py install
成功すると、実行した Python ディレクトリの以下のパスに pyd ファイルと helloext-(バージョン)-(ビルド環境).egg-info ファイルが追加されます。
(Pythonディレクトリ)\Lib\site-packages
モジュールの実行確認
python を起動して以下のコマンドを実行します。
import helloext print(helloext)
インストールしたモジュールへのパスが表示されればインストールは成功しています。
続けて以下の通り、関数を実行してみます。
helloext.printstrext() helloext.sumargext(2, 4)
それぞれ"Hello World"の出力と、足し算の結果が出力されれば成功です。
更に文字列の関数も実行してみます。
引数を渡すパターンと、渡さないパターンでそれぞれ戻り値を確認してみます。
ret_str = helloext.printarg() print(ret_str) ret_str = helloext.printarg("Hello Test") print(ret_str)
引数を渡さない場合は"Default"、引数を渡した場合はその文字列が出力されれば成功です。