
Pythonでプログラムを実装したが、処理速度の観点でボトルネックになっている部分があるため、C言語を用いて高速化したい。
また、C言語からPythonで定義した関数も呼び出したい。
これらを実現する方法について教えて欲しい。
こんなお悩みを解決します。
今回は、求根アルゴリズムの1つとして有名なニュートン法を題材に、PythonからC言語の関数を呼び出す方法・C言語からPythonの関数を呼び出す方法について解説します。
Python、C言語の両方から関数を呼び出す方法について解説するので、興味がある方はぜひ最後までご覧ください。
全体構成
下記の2パターンについて解説する上で、全体構成を説明したいと思います。
- PythonからC言語の関数を呼び出すケース
- C言語からPythonの関数を呼び出すケース
PythonからC言語の関数を呼び出すケース
Pythonで実装された関数のうち、C言語に置き換えたい部分を以下のように表現します。

上記の「C言語に置き換えたいPythonのコード」を置き換えて呼び出す場合、呼び出し関係は以下のようになります。

C言語からPythonの関数を呼び出すケース
同様に、C言語から呼び出したいPythonの関数を以下のように表現します。

上記の「C言語から呼び出したいPythonの関数」をC言語から呼び出す場合、呼び出し関係は以下のようになります。

今回は、非線形連立方程式をNewton法で解くことになるため、C言語からPythonの関数を呼び出す必要があります。
したがって、以降では「C言語からPythonの関数を呼び出すケース」を対象に解説を進めます。
対象とする非線形連立方程式
ここでは、以下の非線形連立方程式を解くことを考えます。
$$
\begin{eqnarray}
\left\{
\begin{array}{lcc}
x + 2y + 1 & = & 0\\
x^{2} + 2y^{2} - 3 & = & 0\\
\end{array}
\right.
\end{eqnarray}
$$
また、この解析解は、以下のようになります。
\((x, y) = (1, -1), \left(-\dfrac{5}{3}, \dfrac{1}{3} \right)\)
Pythonによる実装
Pythonによる実装例は、以下のようになります。
今回は、numpy等のライブラリを用いて実装しています。
また、Github上の以下のファイルと対応します。
https://github.com/yuruto-free/python-c-api-newton/blob/v0.1.0/original/newton_method.py
それぞれの関数は、以下のような内容となります。
関数名 | 内容 |
---|---|
objective_function | 目的関数 今回の解く対象となる非線形連立方程式が定義されている。 |
calc_Jacobian_matrix | ヤコビ行列の計算用関数 |
newton_method | ニュートン法により近似解を求める関数 |
また、実行すると以下のような出力が得られます。
C言語に置き換える関数・C言語から呼び出すPython関数
定義したPython関数を「C言語に置き換える関数」、「C言語から呼び出すPython関数」に振り分けると以下のようになります。
関数名 | 呼び出し関係 |
---|---|
objective_function | C言語から呼び出すPython関数 |
calc_Jacobian_matrix | C言語に置き換える関数 |
newton_method | C言語に置き換える関数 |
C言語による実装
ここから、C言語による関数の置き換え方について説明していきます。
また、Github上に一通り格納しているので、参考にしてください。
https://github.com/yuruto-free/python-c-api-newton/tree/v0.1.0/c-api
全体構成
C言語による実装結果をPythonから呼び出すために、C言語による実装結果を共有ライブラリ(.so
)に変換後、Pythonからロードする、という方法を取ります。
このため、以下に示すように、wrapper_newtonlib.so
ファイルを生成する必要があります。

wrapper_newtonlib.so
ファイル作成時の対象範囲ここでは、上記を踏まえたディレクトリ構成にしております。
具体的な全体構成は以下のようになっております。
それぞれのファイルは、以下のような役割を持ちます。
ディレクトリ名 | ファイル名 | 役割 |
---|---|---|
. | compile.sh | C言語ファイルをコンパイルするためのshell script |
newton-method-with-clib.py | C言語での実装結果を用いて非線形連立方程式をNewton法で解くスクリプト | |
setup.py | C言語ファイルをコンパイルするためのPython script | |
libs/include | wrapper_newton.h | C言語でNewton法を実装する際のAPI定義 |
libs/src | wrapper.c | 前処理、関数呼び出し、後処理部分を実装したファイル |
newton_method.c | Newton法本体 |
以降では、以下に示すステップで解説をしていきます。
- 必要なヘッダーの読み込み
- 実装時の注意点
- C言語から呼び出すPython関数の登録
- C言語によるラッパー関数
newton_method
の前処理部分newton_method
におけるコールバック関数newton_method
の後処理部分- モジュール定義部分(本体実装である
newton_method.c
の解説は省略)
必要なヘッダーの読み込み
PythonのオブジェクトをC言語で処理するためには、Pythonのデータ型を扱う変数やC言語のデータ型に変換するための関数が必要になります。
これらは、Pythonのライブラリとして提供されており、以下の形式で利用できるようになります。
実装時の注意点
C言語では、プログラム実装時に用いる変数を定義する必要があるため、実装者が変数を管理します。
一方、Pythonは変数の管理はPython側で良きに処理しています。
具体的には、Pythonスクリプトにおいて、どこからも参照されなくなったオブジェクトは自動的に割り当てが解除されます。
言い換えると、オブジェクトの参照状態が適切に管理されていない場合、解除忘れのオブジェクトがメモリ上に残り続けるまたは、解除済みのオブジェクトにアクセスすることになります。
これは、メモリリークやメモリアクセス違反に繋がります。
C言語でPythonオブジェクトを管理するために、Py_INCREF
やPyDECREF
といった「参照カウント」を増減させるマクロが用意されています。
これらのマクロを適切に呼び出しつつ、Python/C APIを利用することになります。
上記の実装をする上で、Python/C APIが返却する変数のパターンを知っておく必要があります。
以降では、これらのパターンについて解説します。
Python/C APIが返却する変数のパターン
Python/C APIが返却する変数のパターンとして、「New reference」と「Borrowed reference」の2種類があります。
それぞれの違いは、以下のようになります。
対象 | 内容 |
---|---|
New reference | 返却するPythonのオブジェクトに所有権を付与した状態で返す(所有参照)。 不要になったら、 Py_DECREF で参照カウントを減らす必要がある。 |
Borrowed reference | 返却するPythonオブジェクトに所有権を付与しない状態で返す(借用参照)。 参照カウントを減らしてはいけない。 |
また、一部のAPIには、参照を盗む(APIの内部でPy_DECREF
が"勝手に"呼ばれるイメージ)ものも存在します。
ここでは、参照関係の理解のため、リストオブジェクトを題材に事例を取り上げていきたいと思います。
https://docs.python.org/ja/3/c-api/stable.html
リストオブジェクトによる事例
上記のリンクにアクセスすると、以下のAPIが存在することが確認できると思います。



それぞれ、以下のような関係になります。
API名 | 参照関係 | 使用時の注意点 |
---|---|---|
PyList_New | New reference | 所有権が付与されたオブジェクトを返却するため、参照カウントの管理が必要になる。 |
PyList_GetItem | Borrowed reference | 所有権のないオブジェクトを返却する。参照カウントを減らしてはならない。 |
PyList_SetItem | - | 所有権を盗み取るAPIのため、API呼出し後もオブジェクトを利用する場合、呼び出す前にPy_INCREF で参照カウントを増やしておく必要がある。 |
また、Pythonで下記のようなコードをC言語に置き換える場合も注意が必要となります。
上記の内容を素直にC言語で実装すると以下のようになりますが、オブジェクトを又貸ししている状態となるため、実装時には注意が必要です。
以降では、今回の実装内容について解説していきます。
C言語から呼び出すPython関数の登録
C言語からPythonの関数を呼び出すためには、Pythonの関数を登録するC言語の関数が必要になります。
ここでは、グローバル変数に該当する関数を登録し、Newton法を実行する際に参照する方針とします。
実装結果は、以下のようになります。
また、利用方法としては、以下のようになります。
上記を例に引数の処理方法、参照カウントの使い方を解説していきます。
引数の処理方法
Pythonでキーワード引数を利用しない場合、C言語側での宣言方法は以下のようになります。キーワード引数を用いる例は別途、解説します。
ここで、self
はモジュールオブジェクト、args
が関数の引数となります。
今回の例の場合、self
にはset_objective_function
などが登録されています。ほとんど使うことはないかと思います。
引数の処理には、PyArg_ParseTuple
関数を利用します。
下記にある通り、フォーマットを指定してデータを読み込みます。
https://docs.python.org/ja/3/c-api/arg.html
今回、Pythonで定義した関数(=オブジェクト)を引数に取るため、下記のような処理となります。
参照カウントの増減
変数取得後は、参照カウントを増減させ、引数で受け取った関数を登録します。
ここで、Py_XINCREF
、Py_XDECREF
は、NULL
を許容するバージョンのPy_INCREF
、Py_DECREF
となります。
最後に、Py_RETURN_NONE
マクロを用いて関数の戻り値を返却します。
Py_RETURN_NONE
は、戻り値としてNone
を返すマクロであり、以下と同じ働きをします。
C言語によるラッパー関数
以降では、下記のPython関数をC言語に置き換え、Pythonから呼び出すための実装方法について解説します。
C言語による実装を以下に示します。
上記のStep1~Step5までが前処理部分、Step6がコールバック関数の処理部分、Step7が後処理部分となります。
以降で、該当箇所の解説をしていきます。
newton_method
の前処理部分
Step1~Step5に対する解説を行っていきます。
Step1・Step2:引数処理と引数チェック
ここでは、キーワード引数を用いる場合の引数処理について解説します。
該当する処理のみを記載すると以下のようになります。
キーワード引数を用いる場合、C言語側(呼び出し先)では受け取る際のキーワード一覧を用意しておく必要があります。
今回の場合、以下が該当します。
上記の設定により、Python側(呼び出し元)では以下のような振る舞いを持つ関数として利用できます。
また、今回のケースでは、vec
はNumpyオブジェクトのため、期待通りのオブジェクトになっているか、あわせて確認しています。
Numpy/C APIは、以下の公式サイトでまとめられています。
https://numpy.org/doc/stable/reference/c-api/index.html
Step3~Step5:領域確保・データの読み込み
引数処理後は、Numpyオブジェクトからデータを取得し、C言語で確保したメモリ領域にコピーします。
データ領域を無駄に消費しますが、処理自体はC言語だけに閉じるため高速になります。
また、Numpyオブジェクトは、1次元配列を仮定しているため、PyArray_SIZE
やPyArray_GETPTR1
でNumpyオブジェクトから情報を取得しています。
newton_method
におけるコールバック関数
C言語のデータをPythonで扱う為には、データ型の変換が必要になります。
今回は、Python側で定義された関数を呼び出す際に、データ型の変換を行う代替コールバック関数(alternative_callback_function
)を定義しています。
alternative_callback_function
の実装は以下のようになります。
処理自体は、下記に示す通り、シンプルなものとなります。
- 与えられた次元の
np.array
オブジェクトを作成する。 - C言語からの入力データをNumpyオブジェクトの該当インデックスにコピーする。
- あらかじめ登録しておいたPython側の関数(
py_objective_function
)を引数とともに呼び出す。 - 戻り値を確認後、Numpyオブジェクトの内容をC言語の出力データの該当インデックスにコピーする。
newton_method
の後処理部分
推定値が得られたら、呼び出し元であるPythonにデータを返却できるように後処理を行います。
返却するデータを作成する部分は、以下のようになります。
ここでも、参照カウントの考慮が必要になるため、適切なカウント処理を挟みます。
また、PythonのTrue
、False
をC言語でも扱えますが、これらも参照カウントの管理が必要になるため、次のような実装により参照カウントを増やしています。
モジュール定義部分
最後に、PythonからC言語の関数を呼び出すための"つなぎ"の部分について解説します。
モジュール定義時は、以下の3ステップで実現します。
- モジュールに登録する関数の一覧を作成する。
- 作成するモジュール名や内包する関数一覧との対応関係など、モジュールに関する情報を定義する。
- モジュール呼び出し時の初期化処理を定義する。
モジュールに登録する関数一覧
C言語で定義した関数をモジュールに登録する際は、以下の形式の構造体を配列として定義します。
構造体メンバ | C言語での型定義 | 内容 |
---|---|---|
ml_name | const char * | モジュールから呼び出す際のメソッド名 |
ml_meth | PyCFunction | C言語で定義した関数へのポインタ |
ml_flags | int | 関数の呼び出し方を示すフラグビット |
ml_doc | const char * | C言語で定義した関数に対するdocstringの内容を示すポインタ |
今回は2つの関数があるため、それぞれ構造体を定義します。
定義結果は以下のようになります。
モジュールに関する情報の定義
以下の形式の構造体を用いて、Pythonで呼び出す際のモジュールを定義します。
構造体メンバ | C言語での型定義 | 内容 |
---|---|---|
m_base | PyModuleDef_Base | 常にPyModuleDef_HEAD_INIT で初期化する |
m_name | const char * | モジュール名 |
m_doc | const char * | このモジュールに対するdocstringの内容を示すポインタ |
m_size | Py_ssize_t | モジュールごと個別のメモリ領域を持たせる場合に非負の値を指定 今回は、コールバック関数をグローバル変数で管理するため、 -1 を指定 |
m_methods | PyMethodDef * | 「モジュールに登録する関数一覧」で定義した変数を指定 |
今回は、Newton法のラッパー関数として定義するため、「wrapper_newtonlib
」というモジュール名にします。
具体的な定義結果は以下のようになります。
モジュール呼び出し時の初期化処理
最後に、モジュール呼び出し時の処理を記述します。上記のモジュール定義をPyModule_Create
関数の引数に渡し、呼び出せばOKです。
また、今回はNumpyを利用するため、import_array
関数も実行しています。
ライブラリの生成
C言語で実装したものをPythonから呼び出せるように共有ライブラリを生成する準備をします。
共有ライブラリは、setuptools
を用いると簡単に生成できるため、setup.pyを作成後、gccによるコンパイルにより共有ライブラリを生成します。
今回のケースにおける、setup.pyの実装例を以下に示します。
また、コンパイル時は下記のコマンドを実行します。
Pythonからの呼び出し
一通りC言語での実装が完了したため、Pythonから共有ライブラリを読み込み、モジュールを利用する方法について説明します。
全体像は以下のようになります。
このうち、今回の対応事項に関係する部分は以下のようになります。
上記のスクリプトを実行すると以下のような結果が得られます。
Pythonのみで実装した結果と同じ結果が得られていることが分かります。
まとめ
今回は、Python/C APIを用いて、PythonからC言語の関数を呼び出す方法・C言語からPythonの関数を呼び出す方法について解説しました。
参照カウントの管理や所有権の所在等、注意する部分はありますが、C言語で実装することにより、処理速度の改善が期待できるので、処理時間を気にしている方は一度試してみてはどうでしょうか。
また、今回はガウスの消去法を自前で実装しましたが、BLASやLAPACKなどのライブラリを用いることでもっと手軽に処理を実装できます。
ガウスの消去法については、以下で解説しているので、興味がある方は参考にしてください。
-
-
【解説&実装】ガウスの消去法
続きを見る