C ライブラリを使う

Cython には、高速なコードを書く以外にも、外部の C ライブラリを Python のコードから呼び出すという重要なユースケースがあります。 Cython のコードは C のコード自体にコンパイルされるので、コード中で C の関数を直接呼び出すのがきわめて簡単になります。 以下の例では、適切なエラー処理を行い、かつ Python と Cython コードの API 設計を考慮しながら、外部の C ライブラリを Cython のコードから使い (ラップし) ます。

例えば、整数の値を FIFO キューに効率的に格納する必要があったとしましょ う。メモリの制約が厳しく、かつ値は C のコードから直接来るので、Python の int オブジェクトを生成してリストや deque オブジェクトに保存する わけにはいきません。そこで、 C で実装したキューを探すことにします。

Web をしばらくサーチして、C アルゴリズムライブラリ [CAlg] を見つけ、 その中の両端キュー (double ended queue) 実装を使うことに決めました。 ただし、キューを簡単に扱いたいので、Python 拡張型にラップして、メモリ 管理をカプセル化することにします。

キュー実装の C API は libcalg/queue.h に定義されていて、実質的には 以下のようなものです:

/* file: queue.h */

typedef struct _Queue Queue;
typedef void *QueueValue;

Queue *queue_new(void);
void queue_free(Queue *queue);

int queue_push_head(Queue *queue, QueueValue data);
QueueValue queue_pop_head(Queue *queue);
QueueValue queue_peek_head(Queue *queue);

int queue_push_tail(Queue *queue, QueueValue data);
QueueValue queue_pop_tail(Queue *queue);
QueueValue queue_peek_tail(Queue *queue);

int queue_is_empty(Queue *queue);

まず手始めに、 C API を .pxd ファイル、たとえば cqueue.pxd の 中で再定義します:

# file: cqueue.pxd

cdef extern from "libcalg/queue.h":
    ctypedef struct Queue:
        pass
    ctypedef void* QueueValue

    Queue* queue_new()
    void queue_free(Queue* queue)

    int queue_push_head(Queue* queue, QueueValue data)
    QueueValue  queue_pop_head(Queue* queue)
    QueueValue queue_peek_head(Queue* queue)

    int queue_push_tail(Queue* queue, QueueValue data)
    QueueValue queue_pop_tail(Queue* queue)
    QueueValue queue_peek_tail(Queue* queue)

    bint queue_is_empty(Queue* queue)

この宣言はヘッダファイルの内容とほとんど同じなので、たいていは単にコピー してくるだけで事足ります。とはいえ、上の 全ての 宣言を提供する必要は なく、自分のコードや他の宣言の中で必要な分だけでかまわないので、Cython は必要かつ一貫性を失わない範囲で API のサブセットを見ることになります。 次に、この API を Cython でより扱いやすくできるよう加工していきましょ う。

ヘッダファイルと上記のファイルの間の明確な違いは、最初の行の Queue 構造体の宣言です。ここでは、 Queue はいわゆる 非透過的ハンドル(opaque handle) として使われていて、その中が実際どう なっているかは、 Queue を扱うライブラリだけが知っています。 Cython のコードでも構造体の中身を知る必要はないので、データ型の定義で 中身の記述は行わず、(C ヘッダの中で参照している _Queue 型も宣言し たくないので) 単に空の定義とします [1]

[1]cdef struct Queue: passctypedef struct Queue: pass には明確な違いがあります。前者は C コード中で struct Queue と表現されるデータ型であり、後者は Queue 型です。 これは Cython からは隠蔽しようのない C 言語の癖です。最近の C ライブラリは、 ctypedef 形式の構造体宣言を使っています。

もう一つ、ヘッダと .pxd で違うのは最後の行です。 queue_is_empty() の戻り値は、実際にはキューが空かどうかを示す C の ブール値、すなわち、ゼロかゼロでないかだけを取り沙汰する値です。 この種の値は、 Cuthon では bint で表現するのが正解です。 bint は、 C 中では通常の int 型ですが、 Python オブジェクトに変換すると Python のブール型オブジェクトである TrueFalse になります。 こうやって .pxd ファイルの中で型宣言をしっかり固めておくと、ファイ ルを使うコードをシンプルに書けます。

.pxd ファイルは、各ライブラリごとに一つ定義するのがお勧めです。API の規模が大きい場合には、ヘッダファイルごと (または、機能グループごと) に一つでもかまいません。そうすれば、他のプロジェクトで再利用しやすくな ります。 また、標準 C ライブラリの C 関数を使ったり、 CPython の C-API 関数を直 接呼び出したりといった、よくある需要をみたすために、 Cython には標準の .pxd ファイルがひととおり付属していて、Cython で扱いやすいように工 夫した宣言で提供しています。主なパッケージには、 cpython, libc, libcpp があります。 NumPy ライブラリにも numpy とい う名前の標準の .pxd ファイルが付属していて、 Cython のコードで使え ます。Cython が提供している .pxd ファイルの一覧は、ソースコードパッ ケージの Cython/Includes/ ディレクトリ下を参照してください。

C ライブラリの API を宣言したら、C のキューをラップする Queue クラスの 設計に移りましょう。この作業は queue.pyx というファイルで行います [2]

[2]この .pyx は、C ライブラリの宣言に使った cqueue.pxd と は別のものについて記述しているので、ファイルの名前も別にせねば なりません。 ある .pyx と同じ名前を持つ .pxd は、 .pyx ファイル内のコードに関する宣言をエクスポートするのに 使います。 cqueue.pxd には C ライブラリの宣言が入っているの で、同じ名前の .pyx ファイルをCython が関連づけないよう、 cqueue.pyx という名前のファイルは作らないでください。

Queue クラスの書き出しは、こんな感じです:

# file: queue.pyx

cimport cqueue

cdef class Queue:
    cdef cqueue.Queue *_c_queue
    def __cinit__(self):
        self._c_queue = cqueue.queue_new()

__init__ ではなく __cinit__ を使っていることに注意してください。 __init__ も定義できますが、必ず実行されるとは限りません (例えば、 サブクラスを作ったときに、親クラスのコンストラクタを呼び忘れることがあ ります)。一方、 C ポインタの初期化忘れは Python インタプリタの重大なク ラッシュにつながることが多いため、 Cython では、インスタンスの生成直後、 CPython が __init__ の呼び出しを検討するよりも前に、 必ず __cinit__ を呼び出します。従って、新しいインスタンスの cdef フィールドを初期化するには __cinit__ を使うのが正解です。 あだし、 __cinit__ はオブジェクトの生成途中に呼び出されるため、 self はまだ完全に組み立てられていないので、 cdef フィールドの 値を操作する以外の目的で self を使わないようにしましょう。

また、上のメソッドにはパラメタがないのに、サブクラスのコンストラクタで は、何らかの引数をとる可能性があるということにも注意してください。 ここでの引数なしの __cinit__() メソッドは特殊なケースで、コンスト ラクタに渡された引数を一切受け取らないだけであり、サブクラスで引数を加 えることを妨げるものではありません。 __cinit__() のシグネチャにパ ラメタを使う場合には、クラス階層中で型のインスタンス化に使われる __init__ メソッドのシグネチャと一致させねばなりません。

ところで、他のメソッドの実装に移る前に、上の実装が安全でないことを理解 するのが肝心です。 queue_new() の呼び出しが何らかの理由でうまくい かないと、このコードはエラーを呑み込んでしまい、おそらく後でクラッシュ を引き起こしてしまいます。 queue_new() 関数のドキュメントによれば、この関数が失敗するのはメモ リが不足しているときだけで、その場合、通常は新しいキューへのポインタを 返すところ、 NULL が返ります。

この危険を排除するには、 MemoryError を送出します [3] 。 そこで、初期化の関数を以下のように変更しましょう:

cimport cqueue

cdef class Queue:
    cdef cqueue.Queue *_c_queue
    def __cinit__(self):
        self._c_queue = cqueue.queue_new()
        if self._c_queue is NULL:
            raise MemoryError()
[3]MemoryError に限った話ですが、例外を送出するために例外イン スタンスを生成しようとすると、そもそもメモリが不足している状態なた めに実際には失敗する可能性があります。幸運にも、 CPython には PyErr_NoMemory() という関数があり、適切な例外を安全に送出してく れます。バージョン 0.14.1 からは、 raise MemoryError あるいは raise MemoryError() と記述すると、Cython が自動的に適切な C-API 呼び出しに置き換えます。それ以前のバージョンでは、標準パッケージ cpython.exc を cimport して、直接呼び出す必要があります。

次にやることは、不要になった (インスタンスへの参照が全て除去された) Queue を片付けるためのコードです。オブジェクトの削除処理側では、 CPython はコールバックを提供していて、これを Cython では特殊メソッド __dealloc__() で使えます。今扱っているケースでは、やるべきことは C Queue の free です。ただし、 init メソッドで正しく初期化が行われた場 合だけ、 free を実行します:

def __dealloc__(self):
    if self._c_queue is not NULL:
        cqueue.queue_free(self._c_queue)

これで、動作する Cython モジュールができ、テストが可能になりました。 コンパイルするには、distutils 用に setup.py スクリプトを作成して、 設定する必要があります。以下に示すのは、 Cython モジュールをコンパイル するもっとも基本的なスクリプトです:

from distutils.core import setup
from distutils.extension import Extension
from Cython.Distutils import build_ext

setup(
    cmdclass = {'build_ext': build_ext},
    ext_modules = [Extension("queue", ["queue.pyx"])]
)

外部の C ライブラリをリンクするモジュールをビルドするには、このスクリ プトを拡張して、必要な設定を入れてやる必要があります。ライブラリ関連の ファイルがが一般的な場所 (例えば、Unixライクなシステムでは /usr/lib/usr/include) に入っているなら、 extension の設定 を:

ext_modules = [Extension("queue", ["queue.pyx"])]

から

ext_modules = [
    Extension("queue", ["queue.pyx"],
              libraries=["calg"])
    ]

に変更します。

ライブラリが「普通の」場所に入っていないのなら、以下のような形で適切な C コンパイラフラグを渡すことで、必要なパラメタを指定できます::

CFLAGS="-I/usr/local/otherdir/calg/include"  \
LDFLAGS="-L/usr/local/otherdir/calg/lib"     \
    python setup.py build_ext -i

モジュールをコンパイルしたら、 import して Queue をインスタンス化でき るようになります:

$ export PYTHONPATH=.
$ python -c 'import queue.Queue as Q ; Q()'

ただし、Queue クラスでできるのは今はこれだけです。これからもっと便利に していきましょう。

クラスの公開インタフェースを実装していくまえに、Pythonが listcollections.deque といったクラスでどんなインタフェースを提供してい るか参考にしてみます。必要なの FIFO キューの機能なので、 append(), peek(), pop(), と、おまけで複数の値を同時に追加できる extend() メソッドぐらいがあればよいでしょう。値は C 側からしかこな いのが分かっているので、当座は cdef メソッドだけで、ストレートな C インタフェースだけをもたせるのがベストです。

C では、あるデータ構造に別のデータを保存するときには、データ要素の型に 関係なく void* とする手法をよくとります。ここでは int の値を保 存したいのですが、 int 型というのはポインタ型と同じサイズなので、 メモリ確保の手間をさぼるためにちょっとトリックを使います: int の値 を void* と相互変換して、値へのポインタを格納する代わりに、値を直 接ポインタの値であるかのように保存してしまいます。

append() メソッドを簡単に実装すると、こうなります:

cdef append(self, int value):
    cqueue.queue_push_tail(self._c_queue, <void*>value)

ここでも、 __cinit__() と同様に、エラーのハンドリングについて考え てやる必要があります。その結果、以下ようになります:

cdef append(self, int value):
    if not cqueue.queue_push_tail(self._c_queue,
                                  <void*>value):
        raise MemoryError()

extend() メソッドの追加方法も、これにならって書けますね:

cdef extend(self, int* values, size_t count):
    """Append all ints to the queue.
    """
    cdef size_t i
    for i in range(count):
        if not cqueue.queue_push_tail(
                self._c_queue, <void*>values[i]):
            raise MemoryError()

こうすれば、例えば NumPy アレイからデータを読むのに便利です。

今のところ、データをキューに追加できるだけです。次はキューの先頭の要素 を取得する二つのメソッド: peek()pop() を書きましょう。これ らのメソッドは、それぞれ読み出し専用と、破壊的 (destructive) なリード アクセスを行います:

cdef int peek(self):
    return <int>cqueue.queue_peek_head(self._c_queue)

cdef int pop(self):
    return <int>cqueue.queue_pop_head(self._c_queue)

実に簡単です。ところで、キューが空のときはどうなるんでしょう。ドキュメ ントによれば、キューが空のときには NULL ポインタが返ります。 この値は一般的にはポインタとして無効な値です。実のところ、今は値を int 型からキャストするというトリックを使っているので、戻り値が NULL の 場合に、キューが空なのか、キューの先頭の値が 0 なのかを区別できま せん。しかし、われわれは、Cython のコードが前者のケースでは例外を送出 し、後者のケースでは 0 を返すようにしたいです。そこで、この値を 0 特別扱いして、キューが本当に空なのかチェックする必要があります:

cdef int peek(self) except? -1:
    cdef int value = \
      <int>cqueue.queue_peek_head(self._c_queue)
    if value == 0:
        # this may mean that the queue is empty, or
        # that it happens to contain a 0 value
        if cqueue.queue_is_empty(self._c_queue):
            raise IndexError("Queue is empty")
    return value

値が 0 でない、コモンケースと期待される状況については、処理のファー ストパススルーが効率的に作成できていることに注意してください。特殊なケー スでだけ、キューが空かどうかを追加でチェックしています。

メソッドシグネチャ中の except? -1 の宣言は、戻り値の扱いと同じよう なからくりを使っています。 関数が Python のオブジェクト値を返す Python の関数だったら、 CPython は Python オブジェクトを返す代わりに内部的に NULL を返し、例外が送 出されたことを示します。このことは、この処理を内包するコードにすぐに伝 播します。問題は、戻り値は int であり、 int の値は常にキューの 要素の有効な値とみなされるので、関数呼び出し側のコードに明にエラーを通 知する方法がないということです。実際、 except? -1 のような宣言がな ければ、Cython は、このメソッドで例外が起きた際に何を返すかを知らせる ことができず、呼び出し側に、メソッドが例外を送出して実行を終了した かもしれない という通知すらできません。

呼び出し側のコードで、メソッド内で起きたエラーを処理するには、 PyErr_Occurred() を呼び出して、メソッドの中で送出された例外がない かチェックして、もし例外が送出されていれば伝播するという方法をとります。 いうまでもなく、これはパフォーマンス上のペナルティを生みます。 そこで、Cython では、例外が発生したときに、どんな値を暗黙のうちに返す べきかを宣言できるようにして、呼び出し側のコードが、例外が送出されてい る かもしれない 戻り値を受け取った時にだけ、例外が発生していないか チェックすればよいようにしているのです。

ここで、 -1 を「例外送出の可能性がある戻り値 (exception return value)」にしたのは、この値をあまりキューに入れないからです。 except? -1 宣言中の ? は、戻り値が例外送出を示しているか 不明 (ambiguous) であり (実際、キューに -1 という値が入っていたの かもしれない) 、呼び出し側で PyErr_Occurred() を使った追加のチェッ クが必要であることを示しています。チェックを行わなければ、このメソッド を呼び出して、例外送出の可能性がある戻り値を黙って受け取っている Cython コードは、メソッド内で例外が送出されたと (実際には違う場合があ るのに) 仮定することになります。いずれにせよ、 -1 以外の値では、ほ とんど処理上のペナルティを受けることなく値が渡されるので、「通常の」値 むけのファーストパスを生成できています。

さて、 peek() メソッドを実装したので、 pop() にも同様の調整を ほどこしましょう。 pop() はキューから値を除去するので、 除去 にキューが空であるかをチェックするだけでは不十分です。処理の入り口で、 まずテストせねばなりません:

cdef int pop(self) except? -1:
    if cqueue.queue_is_empty(self._c_queue):
        raise IndexError("Queue is empty")
    return <int>cqueue.queue_pop_head(self._c_queue)

例外を伝播するための戻り値は peek() と同様に宣言しておきます。

最後に、特殊メソッド __bool__() を実装して、Queue に、キューが空か どうかを Python で調べるインジケータを作ります (Python 2 ではこのメソッドを __nonzero__ と呼んでいますが、Cython ではどちらの名前も使えます):

def __bool__(self):
    return not cqueue.queue_is_empty(self._c_queue)

cqueue.pxd の中で queue_is_empty 関数の戻り値の型を bint にしておいたので、このメソッドは True または False を返します。

実装が完成したので、正しく動作するかテストを書いて確認しましょう。 テストには、同時にドキュメントを提供できる doctest が特にお勧めです。 ただし、 doctest を有効にするには、 Python から呼び出せるような API が 必要です。 C のメソッドは Python のコードから見えないので、 doctest か らは呼び出せません。

簡単にクラスに Python API 持たせるには、メソッドを cdef から cpdef に変更します。 cpdef に変更すると、Cython は二通りのエ ントリポイントを生成します。一つは通常の Python コードから呼び出せるも ので、 Python の呼び出しセマンティクスを使い、引数に Python のオブジェ クトを取ります。もう一つは C から呼び出せて、高速な C のセマンティクス を使い、 Python のデータ型との間で引数の変換が必要ないものです。

cpdef 型のメソッドをできるだけ使った実装の全体像を以下に示します:

cimport cqueue

cdef class Queue:
    """A queue class for C integer values.

    >>> q = Queue()
    >>> q.append(5)
    >>> q.peek()
    5
    >>> q.pop()
    5
    """
    cdef cqueue.Queue *_c_queue
    def __cinit__(self):
        self._c_queue = cqueue.queue_new()
        if self._c_queue is NULL:
            raise MemoryError()

    def __dealloc__(self):
        if self._c_queue is not NULL:
            cqueue.queue_free(self._c_queue)

    cpdef append(self, int value):
        if not cqueue.queue_push_tail(self._c_queue,
                                      <void*>value):
            raise MemoryError()

    cdef extend(self, int* values, size_t count):
        cdef size_t i
        for i in xrange(count):
            if not cqueue.queue_push_tail(
                    self._c_queue, <void*>values[i]):
                raise MemoryError()

    cpdef int peek(self) except? -1:
        cdef int value = \
            <int>cqueue.queue_peek_head(self._c_queue)
        if value == 0:
            # this may mean that the queue is empty,
            # or that it happens to contain a 0 value
            if cqueue.queue_is_empty(self._c_queue):
                raise IndexError("Queue is empty")
        return value

    cdef int pop(self) except? -1:
        if cqueue.queue_is_empty(self._c_queue):
            raise IndexError("Queue is empty")
        return <int>cqueue.queue_pop_head(self._c_queue)

    def __bool__(self):
        return not cqueue.queue_is_empty(self._c_queue)

extend() のシグネチャや Python の引数型と一致しないので、 cpdefextend() には使えません。ただ、必要であれば、 C 向きの extend() メソッドを別の名前、例えば c_extend() にして、新たに extend() を定義し、 Python の iterable を引数に取れ るようにはできます:

cdef c_extend(self, int* values, size_t count):
    cdef size_t i
    for i in range(count):
        if not cqueue.queue_push_tail(
                self._c_queue, <void*>values[i]):
            raise MemoryError()

cpdef extend(self, values):
    for value in values:
        self.append(value)

筆者のマシンで 10000 個の数値を使って簡単にテストしたところ、この Queue を Cython コードの中で使い、 C の int を扱った場合は、 Python オブジェクトを扱う場合の約 5 倍の速度、 Python のループ中で Python コードから extend する場合に比べて約 8 倍、よく最適化されて いる Python の collections.deque 型を Cython コード中で使って Python の整数型を扱った場合に比べても倍以上早いという結果になりました。

[CAlg]Simon Howard, C Algorithms library, http://c-algorithms.sourceforge.net/

Previous topic

C の関数を呼び出す

Next topic

拡張型の定義 (cdef クラスの定義)

This Page