読者です 読者をやめる 読者になる 読者になる

座敷牢日誌

都落ちした元SEがソフトウェアやネット関連のことを書いています

Python C APIでイテレータを実装する (後編)

Python

PythonC/C++ APIによるイテレータを実装する話の続きです。

ここにあるコードの多くは、オフィシャルのドキュメントを参考にしています。

ここまではチュートリアルで、イテレータ用の __iter__ 相当, __next__ 相当のメソッドの 話までは載ってません。 詳細はリファレンスに網羅されているので、詳しくはそっちを参考にしました。

setup.py を書く

linuxの場合、python用のモジュールをビルドすると、共有ライブラリ *.so.1 みたいな ファイルが作られることになります。 ビルドの仕組みは、distutils モジュールを使う setup.py を使うのが一般的です。 3. distutils による C および C++ 拡張モジュールのビルド — Python 3.4.2 ドキュメント

ドキュメントにあるサンプルコードとほとんど変わりませんが、次のようなファイル setup.py を用意しました。

from distutils.core import setup, Extension

mod_math2 = Extension('math2',
        include_dirs = ['local/include'],
        library_dirs = ['local/lib'],
        libraries = ['math2'],
        sources = ['math2.c'])

setup(name = 'PackageExample',
        version = '1.0',
        description = '',
        author = 'Kosuke Uchida',
        author_email = 'test/localhost',
        url = 'http://zashikiro.hateblo.jp',
        long_description = '',
        ext_modules = [mod_math2])

Extension の引数 sources で指定しているCソースは、Pythonモジュールを作るために用意したものです。

ビルドする場合は、$ python setup.py build を実行します。

Pythonモジュール用 math2.c を書く

前編ではCで、素数判定用と指定した個数の素数を返す関数を宣言・定義しました。

math2_Bool math2_isPrime(int n);
void math2_Primes_initialize(math2_Primes *pThis, int begin, int length);
math2_Bool math2_Primes_next(math2_Primes *pThis, int *value);

おさらいとして簡単に説明すると、 math2_isPrime は関数で、素数かどうかを返します。

math2_Primes は構造体で、クラスの属性などを設定しているつもり。 Cの作法的にかなってないかもしれませんが、Primesクラスと呼称させてください。

math2_Primes_initialize 関数は、Primesクラスの初期化用メソッド。 第1引数は math2_Primes 構造体のポインタです。 メモリの面倒は呼び出し側で見るようにしているので、メモリを確保したうえで呼び出す。

math2_Primes_next 関数は、Primesクラスのメソッドで、呼び出すごとに引数 value素数を格納します。戻り値は返せる素数があれば真 (1) を、なければ偽 (0) を戻ります。

簡単に使用例を示します。initialize で開始する値 0 を、ほしい素数の個数に 10 を指定。 すると、0から素数の値が出力されるという結果になります。

    int n;
    math2_Primes *primes;

    math2_Primes_initialize(primes, 0, 10);
    while(math2_Primes_next(primes, &n))
        printf("%d\n", n);
    return 0;

これらをスタティックライブラリとしてビルドしています。

必要なヘッダーをインクルードする

#include <Python.h>
#include "structmember.h"
#include <math2.h>

Python.hPython用モジュールを作るために必ず必要なヘッダーです。

structmember.h はクラスの属性を扱うための宣言の多くが入っています。

ユーザ定義クラスではなく、関数を作るだけならほとんどの場合は必要ないと思います。

Primesクラスの属性を設定する

ここからは、ほとんどドキュメントの記載に従っています。

始めにユーザ定義クラスに持たせるメンバを定義します。ここでの想定は、Cで作ったクラス相当の 構造体ポインタをPythonで使えるようPythonAPIでラップするということなので、 構造体ポインタだけをメンバにしました。

一番最初の PyObject_HEAD はマクロで展開されるメンバです。とりあえずチュートリアルに従って入れています。

typedef struct {
    PyObject_HEAD
    math2_Primes *primes;
} math2_py_Primes;

ついでにここで書いておきますが、APIユーザは PyPy_ で始まる定義を作ってはいけないそうです。 ただちに問題にはならないとは思いますが、API側がアップデートされることを考えると従ったほうがいいです。

私はCのコードの非スタティックなオブジェクトの名前を、モジュール名_関数名 または モジュール名_クラス名_メソッド名 という形にしました。

PythonAPIのコードのなかでは、間に _py_ を入れることにしました。このへんはちょっと見にくくて。 後になって思ったんですけど、小文字でプリフィックス py_ なら大丈夫かな。

デストラクタ __del__ 相当の関数を定義する

static void math2_py_Primes_dealloc(math2_py_Primes *self)
{
    PyMem_Free(self->primes);
    Py_TYPE(self)->tp_free((PyObject*)self);
}

デストラクタです。Pythonでわざわざ __del__ を書く場面は少ないですが。 チュートリアルではメンバを持つクラスとして最低限必要なものとして記載されています。

コンストラクタ __init__ 相当の関数を定義する

static int math2_py_Primes_init(math2_py_Primes *self, PyObject *args, PyObject *kwds)
{
    int begin, length;
    self->primes = (math2_Primes*)PyMem_Malloc(sizeof(math2_Primes));
    if(!PyArg_ParseTuple(args, "ii", &begin, &length))
        return -1;
    math2_Primes_initialize(self->primes, begin, length);
    return 0;
}

やっていることはメンバ変数のメモリ確保と、引数のパースと、Cで作った初期化関数を呼び出すことです。

PyMem_Malloc でメモリ確保するのが正しいかはちょっとわかってないのですが、malloc は使うなと、 そのかわりにこれを使えと書かれてました。free も同様で、PyMem_Free を使えとのことです。

PyArg_ParseTuple はPythonAPIの引数パース用の関数です。 キーワード引数がある場合は、また違う指定方法があるので必要なら調べてください。

イテレータ用関数 __iter__ 相当

static PyObject* math2_py_Primes_iter(PyObject *self)
{
    Py_INCREF(self);
    return self;
}

このあたりはPythonで書く __iter__ と同じに見えないでしょうか。インスタンス自身を戻す必要があるという 点は同じだと思います。

もうひとつのイテレータ用関数 __next__ 相当

static PyObject* math2_py_Primes_iternext(PyObject *self)
{
    int value;
    math2_py_Primes *_self = (math2_py_Primes*)self;
    if(math2_Primes_next(_self->primes, &value))
        return PyLong_FromLong(value);
    else
    {
        PyErr_SetNone(PyExc_StopIteration);
        return NULL;
    }
}

これもPythonとほとんど変わりません。値は return で戻し、戻せる値がなくなったら、 例外 StopIteration を送出するところは全く同じです。

タイプオブジェクトを定義する

static PyTypeObject math2_py_PrimesType = {
    PyVarObject_HEAD_INIT(NULL, 0)
    "math2.Range",             /* tp_name */
    sizeof(math2_py_Primes), /* tp_basicsize */
    0,                         /* tp_itemsize */
    (destructor)math2_py_Primes_dealloc,                         /* tp_dealloc */
    0,                         /* tp_print */
    0,                         /* tp_getattr */
    0,                         /* tp_setattr */
    0,                         /* tp_reserved */
    0,                         /* tp_repr */
    0,                         /* tp_as_number */
    0,                         /* tp_as_sequence */
    0,                         /* tp_as_mapping */
    0,                         /* tp_hash  */
    0,                         /* tp_call */
    0,                         /* tp_str */
    0,                         /* tp_getattro */
    0,                         /* tp_setattro */
    0,                         /* tp_as_buffer */
    Py_TPFLAGS_DEFAULT,        /* tp_flags */
    "Primes 's docs",           /* tp_doc */

    0,                         /* tp_traverse */
    0,                         /* tp_clear */
    0,                         /* tp_richcompare */
    0,                         /* tp_weaklistoffset */
    math2_py_Primes_iter,                         /* tp_iter */
    math2_py_Primes_iternext,                         /* tp_iternext */
    0,             /* tp_methods */
    0,             /* tp_members */
    0,                         /* tp_getset */
    0,                         /* tp_base */
    0,                         /* tp_dict */
    0,                         /* tp_descr_get */
    0,                         /* tp_descr_set */
    0,                         /* tp_dictoffset */
    (initproc)math2_py_Primes_init,      /* tp_init */
    0,                         /* tp_alloc */
    0,                 /* tp_new */
};

ここで、これまでに作ったメソッド用の関数をセットします。リファレンスにあるコードをコピペして、 必要な部分を作った関数に置き換えるという使い方でよろしいかと。

ここで定義するのはPythonの基底クラスである object で標準で使えるメソッドです。 今回はイテレータ用のメソッドだけしか用意していませんが、自分で用意したメソッドは 違う方法で設定することになっています。リファレンスを参照してください。

先頭はAPI提供のマクロで、何も考えず入れてよろしいかと思います。

    PyVarObject_HEAD_INIT(NULL, 0)

名称です。type(math2.Primes(0, 10)) などと打つと返される値です。

    "math2.Primes",             /* tp_name */

オブジェクト生成時に必要なメモリ量を指定します。

    sizeof(math2_py_Primes), /* tp_basicsize */

デストラクタ。

    (destructor)math2_py_Primes_dealloc,                         /* tp_dealloc */

ビットマスクで、継承可能なクラスを作りたければ Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE などとするようです。詳しく試してはいません。

    Py_TPFLAGS_DEFAULT,        /* tp_flags */

ドキュメント用のテキスト。help() とかで出てくるテキストになります。

    "Primes 's docs",           /* tp_doc */

イテレータ用の関数。

    math2_py_Primes_iter,                         /* tp_iter */

イテレータ用の関数、その2。

    math2_py_Primes_iternext,                         /* tp_iternext */

コンストラクタ __init__ 相当の関数。

    (initproc)math2_py_Primes_init,      /* tp_init */

(おまけ) isPrime関数も利用可能にする

static PyObject* math2_py_isPrime(PyObject *self, PyObject *args)
{
    int n;
    if(!PyArg_ParseTuple(args, "i", &n))
        return NULL;
    return PyBool_FromLong(math2_isPrime(n));
}

Cで作った素数判定関数 isPrime も使えるようにしておきたいので、定義しておきました。

メソッド定義、モジュール定義、モジュール初期化関数

static PyMethodDef math2Methods[] = {
    {"isprime", math2_py_isPrime, METH_VARARGS, "isprime 's docs"},
    {NULL, NULL, 0, NULL},
};

static struct PyModuleDef math2Module = {
    PyModuleDef_HEAD_INIT,
    "math2",
    NULL,
    -1,
    math2Methods,
};

PyMODINIT_FUNC PyInit_math2(void)
{
    PyObject *m;
    m = PyModule_Create(&math2Module);
    if(m == NULL)
        return NULL;

    math2_py_PrimesType.tp_new = PyType_GenericNew;
    if(PyType_Ready(&math2_py_PrimesType) < 0)
        return NULL;

    Py_INCREF(&math2_py_PrimesType);
    PyModule_AddObject(m, "Primes", (PyObject*)&math2_py_PrimesType);

    return m;
}

ここらへんはお決まりのコードだと思うので、まとめて。 PyMethodDef[] でモジュールに含める関数を列挙します。 PyModuleDef でモジュール定義を記載します。

最後の唯一の非staticな関数 PyInit_math2 は初期化用の関数です。 PyInit_モジュール名 という名前にする必要があります。 モジュールに含めるクラスはここに記載する必要があります。

math2_py_PrimesType.tp_new = PyType_GenericNew;
if(PyType_Ready(&math2_py_PrimesType) < 0)
    return NULL;

Py_INCREF(&math2_py_PrimesType);
PyModule_AddObject(m, "Primes", (PyObject*)&math2_py_PrimesType);

例外クラスなどがあれば、それもここに追記します。

ビルドする

ここまででビルドが可能になります。 $ python setup.py build を実行すると、 build/lib.アーキテクチャ名pythonでインポート可能なモジュールが作られます。

まとめ

ここまででとりあえず実行可能なイテレータオブジェクトを作ることができます。

ピュアPythonで書かれた同じ結果を返すコードと、time コマンドで実行速度を比較したところ、 およそ40倍C/C++ APIで作ったモジュールのほうが速いという結果になりました。

作ったコードで気になるところとして、メモリ管理のやりかたはあってるのか、リークしてないか……などがあります。 デバッグ方法などもリファレンスにあるので、時間があれば調べてみようかなと思います。

作成したソースをbitbucketに置いたので、必要なら参考にしてみてください。ツッコミ歓迎です。

https://kuchida1981@bitbucket.org/kuchida1981/python-capi-examples.git

広告を非表示にする