カスタムC拡張でPythonを強化する

この記事では、PythonのC言語拡張を構築するために使用されるCPythonのC APIにハイライトを当てるつもりです。私は、かなり平凡な、おもちゃのようなC関数の小さなライブラリを取って、Pythonラッパーに公開するための一般的なワークフローについて説明します。

不思議に思うかもしれませんが… Pythonは何でもできる素晴らしい高級言語なのに、なぜ面倒なCのコードを扱いたいのだろう?そして、私はその主張の大前提に同意しなければならないでしょう。(i) Pythonコードの特定の遅い部分をスピードアップするため、(ii) すでに確立されたPythonプログラムにCで書かれたプログラムを含まざるを得ず、PythonでCコードを書き直したくない場合です。後者は、最近私に起こったことで、私が学んだことを皆さんと共有したいと思います。

主要なステップのまとめ

  1. C言語のコードを入手する、または書く
  2. Python C API ラッパー関数を書く
  3. 関数テーブルの定義
  4. モジュールの定義
  5. 初期化関数の作成
  6. 拡張機能のパッケージ化とビルド

Cコードの取得または記述

このチュートリアルでは、私の限られたC言語の知識で書いた小さなC関数のセットを扱います。これを読んでいるすべてのCプログラマは、これから見るコードに対して私に同情してください。

// demolib.h
unsigned long cfactorial_sum(char num_chars[]);
unsigned long ifactorial_sum(long nums[], int size);
unsigned long factorial(long n);


#include <stdio.h>
#include "demolib.h"


unsigned long cfactorial_sum(char num_chars[]) {
    unsigned long fact_num;
    unsigned long sum = 0;


for (int i = 0; num_chars[i]; i++) {
        int ith_num = num_chars[i] - '0';
        fact_num = factorial(ith_num);
        sum = sum + fact_num;
    }
    return sum;
}


unsigned long ifactorial_sum(long nums[], int size) {
    unsigned long fact_num;
    unsigned long sum = 0;
    for (int i = 0; i &lt; size; i++) {
        fact_num = factorial(nums[i]);
        sum += fact_num;
    }
    return sum;
}


unsigned long factorial(long n) {
    if (n == 0)
        return 1;
    return (unsigned)n * factorial(n-1);
}


最初のファイル demolib.h は C のヘッダーファイルで、これから扱う関数のシグネチャを定義し、2 番目のファイル demolib.c はそれらの関数の実際の実装を示します。

最初の関数 cfactorial_sum(char num_chars[]) は、chars の配列で表される数値のC文字列を受け取り、各 char は数値となります。この関数は,各文字列をループしてint型に変換し, factorial(long n) によってint型の階乗を計算し,それを累積和に加えることで和を計算します.最後に、この関数を呼び出したクライアントコードに合計を返します。

2番目の関数 ifactorial_sum(long nums[], int size)sfactorial_sum(...) と似たような動作をしますが、int型への変換は必要ありません。

最後の関数は、再帰的な型アルゴリズムで実装された単純な factorial(long n) 関数です。

Python C API ラッパーファンクションの書き方

CからPythonへのラッパー関数を書くことは、これから説明するプロセス全体の中で最も複雑な部分です。私が使用するPython C拡張APIは、ほとんどのCPythonのインストールに含まれているCヘッダーファイルのPython.hにあります。このチュートリアルでは、CPython 3.6のanacondaディストリビューションを使用する予定です。

まず最初に、Python.hヘッダーファイルをdemomodule.cという新しいファイルの先頭にインクルードします。また、カスタムヘッダーファイルdemolib.hも、私がラップする関数へのインターフェースのようなものとしてインクルードします。また、作業しているすべてのファイルは同じディレクトリにあるべきであると付け加えておきます。

// demomodule.c
#include <python.h>
#include "demolib.h"


それでは、最初のC関数 cfactorial_sum(...) のラッパーの定義に取りかかりましょう。この関数は静的である必要があり、そのスコープはこのファイルだけに限定され、Python.hヘッダーファイルを介してプログラムに公開される PyObject を返す必要があります。ラッパー関数の名前は DemoLib_cFactorialSum で、2つの引数を持ちます。両方とも PyObject 型で、最初の引数は self へのポインタ、2番目は呼び出した Python コードから関数に渡される引数へのポインタです。

static PyObject *DemoLib_cFactorialSum(PyObject *self, PyObject *args) {
    ...
}


次に、クライアントのPythonコードがこの関数に渡す数字の文字列を解析して、C chars配列に変換する必要があります。これは、階乗和を返すために cfactorial_sum(...) 関数が使用できるようにします。これは PyArg_ParseTuple(...) を使って行うことになります。

まず、関数に渡されるPythonの文字列の内容を受け取るために、 char_nums というCの文字ポインタを定義する必要があります。次に PyArg_ParseTuple(...) を呼び出し、 PyObject args の値を渡します。args の最初の(そして唯一の)パラメータが、最後の引数である char_nums 変数に強制されるべき文字列であると指定したフォーマット文字列 "s" を渡します。

もし PyArg_ParseTuple(...) でエラーが発生したら、適切な型エラー例外が発生し、戻り値は 0 になります。これは、条件式の中では false と解釈されます。もし、私の if 文でエラーが検出されたら、私は NULL を返し、呼び出した Python コードに例外が発生したことを知らせます。

static PyObject *DemoLib_cFactorialSum(PyObject *self, PyObject *args) {
    char *char_nums;
    if (!PyArg_ParseTuple(args, "s", &amp;char_nums)) {
        return NULL:
    }
}


ここで少し時間をとって、 PyArg_ParseTuple(...) 関数がどのように動作するかについてお話したいと思います。私はこの関数について、クライアントのPython関数に渡され、 PyObject *args パラメータで捕捉された可変数の位置引数を取るというメンタルモデルを作りました。そして、*args パラメータで取り込まれた引数は、フォーマット文字列指定子の後にある C で定義された変数に展開されると考えます。

以下の表は、私がより一般的に使用されると思うフォーマット指定子です。

| — | — | — |
| c|char|長さ1のPython文字列をC charに変換したもの
| s|char配列|Pythonの文字列をCのchar配列に変換|d|double|Pythonの文字列をCのchar配列に変換
| d | double|C言語のdoubleに変換されたPythonのfloat|。
| f|float|C言語のfloatに変換されたPythonの浮動小数点|i|int|C言語のintに変換されたPythonの浮動小数点
| i|int|C言語のintに変換されたPythonのint| l|long|C言語のintに変換されたPythonのlong
| l|long|C言語のlongに変換されたPythonのint||。
| o | PyObject * | Python object converted to a C PyObject | 複数の引数を渡す場合、PythonのオブジェクトをCのオブジェクトに変換します。

C の型に展開され強制される複数の引数を関数に渡す場合は、単に PyArg_ParseTuple(args, "si", &amp;charVar, &amp;intVar) のような複数の指定子を使用するだけでよいです。

さて、PyArg_ParseTuple(...) がどのように動作するのかを理解できたので、先に進みます。次に行うことは、 cfactorial_sum(...) 関数を呼び出して、先ほどラッパーに渡された Python 文字列から作成した char_nums 配列を渡すことです。戻り値は unsigned long です。

static PyObject *DemoLib_cFactorialSum(PyObject *self, PyObject *args) {
    // arg parsing omitted
    unsigned long fact_sum;
    fact_sum = cfactorial_sum(char_nums);
}


DemoLib_cFactorialSum(…)ラッパー関数で最後に行うことは、クライアントのPythonコードが作業できる形で合計を返すことです。これを行うために、私はPython.hの宝物庫から公開されているPy_BuildValue(…)という別のツールを使っています。Py_BuildValuePyArg_ParseTuple(...) が使用する方法と非常によく似たフォーマット指定子を使用しますが、ただ方向が逆なだけです。また、 Py_BuildValue はタプルやディクスのようなおなじみのPythonデータ構造を返すことができます。このラッパー関数では、Pythonにintを返すことにして、次のように実装しています。

static PyObject *DemoLib_cFactorialSum(PyObject *self, PyObject *args) {
    // arg parsing omitted


// factorial function call omitted


return Py_BuildValue("i", fact_sum);
}


以下は、その他の戻り値のフォーマットや型の例です。

| ラッパーコード|Pythonに返す|||その他の戻り値の形式と型の例です。
| — | — |
| Py_BuildValue(“s”, “A”) | “A” | [英語版
| Py_BuildValue(“i”, 10) | “10
| Py_BuildValue(“(iii)”, 1, 2, 3) | (1, 2, 3) |
| Py_BuildValue(“{si,si}”, “a’, 4, “b”, 9) | {“a”: 4, “b”: 9}
| Py_BuildValue(“”) | None | です。

かっこいいでしょう!?

では、もう1つのC関数 ifactorial_sum(...) のラッパーの実装に取りかかりましょう。このラッパーは他にもいくつかの癖を含んでいるので、それを克服する必要があります。

static PyObject *DemoLib_iFactorialSum(PyObject *self, PyObject *args) {
    PyObject *lst;
    if(!PyArg_ParseTuple(args, "O", &amp;lst)) {
        return NULL;
    }
}


見ての通り、関数のシグネチャは静的で、PyObjectを返し、パラメータは2つのPyObjectであるという点で、前回の例と同じです。しかし、引数のパース処理は

定義関数表

このセクションでは、前のセクションで書いた2つのラッパー関数を、Pythonで公開される名前に関連付ける配列を書き出すことにします。この配列はまた、関数に渡される引数の型、 METH_VARARGS と関数レベルの doc 文字列を提供します。

    int n = PyObject_Length(lst);
    if (n &lt; 0) {
        return NULL;
    }


モジュール定義

ここでは、先に定義した DemoLib_FunctionsTable 配列をモジュールに関連付けるモジュール定義を行います。この構造体は、Python で公開されるモジュールの名前を定義したり、モジュールレベルの doc 文字列を与える役割も担っています。

  long nums[n];
  for (int i = 0; i &lt; n; i++) {
    PyLongObject *item = PyList_GetItem(lst, i);
    long num = PyLong_AsLong(item);
    nums[i] = num;
  }


初期化関数を書き込む

最後に書くべき C 言語っぽいコードは、モジュールの初期化関数で、これはラッパーコードの中で唯一の非静的なメンバです。この関数は PyInit_name という非常に特殊な命名規則を持っていて、 name はモジュールの名前です。この関数は Python インタープリターで呼び出され、モジュールを作成し、アクセスできるようにします。

    unsigned long fact_sum;
    fact_sum = ifactorial_sum(nums, n);


return Py_BuildValue("i", fact_sum);


これで、完全なエクステンションコードは以下のようになります。

static PyMethodDef DemoLib_FunctionsTable[] = {
    {
        "sfactorial_sum",      // name exposed to Python
        DemoLib_cFactorialSum, // C wrapper function
        METH_VARARGS,          // received variable args (but really just 1)
        "Calculates factorial sum from digits in string of numbers" // documentation
    }, {
        "ifactorial_sum",      // name exposed to Python
        DemoLib_iFactorialSum, // C wrapper function
        METH_VARARGS,          // received variable args (but really just 1)
        "Calculates factorial sum from list of ints" // documentation
    }, {
        NULL, NULL, 0, NULL
    }
};


Extensionのパッケージングとビルド

それでは、拡張モジュールをパッケージ化してビルドし、Python で setuptools ライブラリの助けを借りて使えるようにします。

まず最初に setuptools をインストールする必要があります。

static struct PyModuleDef DemoLib_Module = {
    PyModuleDef_HEAD_INIT,
    "demo",     // name of module exposed to Python
    "Demo Python wrapper for custom C extension library.", // module documentation
    -1,
    DemoLib_FunctionsTable
};


次に setup.py という新しいファイルを作成します。以下は、私のファイルがどのように構成されているかを表したものです。

PyMODINIT_FUNC PyInit_demo(void) {
    return PyModule_Create(&amp;DemoLib_Module);
}


setup.pyでは、setuptoolsからExtensionクラスとsetup関数をインポートしています。このクラスは、ほとんどのUnix系OSにネイティブでインストールされているgccコンパイラを使用してCコードをコンパイルするために使用されます。WindowsユーザーはMinGWをインストールすることをお勧めします。

最後に示すコードは、コードをPythonパッケージにするために必要な最小限の情報を渡すだけです。

#include <stdio.h>
#include <python.h>
#include "demolib.h"


// wrapper function for cfactorial_sum
static PyObject *DemoLib_cFactorialSum(PyObject *self, PyObject *args) {
    char *char_nums;
    if (!PyArg_ParseTuple(args, "s", &amp;char_nums)) {
        return NULL;
    }


unsigned long fact_sum;
    fact_sum = cfactorial_sum(char_nums);


return Py_BuildValue("i", fact_sum);
}


// wrapper function for ifactorial_sum
static PyObject *DemoLib_iFactorialSum(PyObject *self, PyObject *args) {
    PyObject *lst;
    if (!PyArg_ParseTuple(args, "O", &amp;lst)) {
        return NULL;
    }


int n = PyObject_Length(lst);
    if (n &lt; 0) {
        return NULL;
    }


long nums[n];
    for (int i = 0; i &lt; n; i++) {
        PyLongObject *item = PyList_GetItem(lst, i);
        long num = PyLong_AsLong(item);
        nums[i] = num;
    }


unsigned long fact_sum;
    fact_sum = ifactorial_sum(nums, n);


return Py_BuildValue("i", fact_sum);
}


// module's function table
static PyMethodDef DemoLib_FunctionsTable[] = {
    {
        "sfactorial_sum", // name exposed to Python
        DemoLib_cFactorialSum, // C wrapper function
        METH_VARARGS, // received variable args (but really just 1)
        "Calculates factorial sum from digits in string of numbers" // documentation
    }, {
        "ifactorial_sum", // name exposed to Python
        DemoLib_iFactorialSum, // C wrapper function
        METH_VARARGS, // received variable args (but really just 1)
        "Calculates factorial sum from list of ints" // documentation
    }, {
        NULL, NULL, 0, NULL
    }
};


// modules definition
static struct PyModuleDef DemoLib_Module = {
    PyModuleDef_HEAD_INIT,
    "demo",     // name of module exposed to Python
    "Demo Python wrapper for custom C extension library.", // module documentation
    -1,
    DemoLib_FunctionsTable
};


PyMODINIT_FUNC PyInit_demo(void) {
    return PyModule_Create(&amp;DemoLib_Module);
}


シェルで次のコマンドを実行し、パッケージをビルドしてシステムにインストールします。このコードは setup.py ファイルを探し、その setup(...) 関数を呼び出します。

$ pip install setuptools


最後に、Pythonインタプリタを起動して、モジュールをインポートし、拡張関数をテストすることができます。

├── demolib.c
├── demolib.h
├── demomodule.c
└── setup.py


結論

最後に、このチュートリアルは Python C API の表面しか見ておらず、巨大で困難なトピックであることがわかったと申し上げたいと思います。Pythonを拡張する必要が出てきたとき、このチュートリアルと公式ドキュメントがその目標達成の助けになればと願っています。

読んでくれてありがとう、そして私は以下のコメントや批判を歓迎します。

</python.h

タイトルとURLをコピーしました