私が歌川です

@utgwkk が書いている

Python C API を使ってみて雰囲気をつかむ

はじめに

最近は機械学習などで Python がよく書かれていますが,その内部構造を意識してコードを書くという機会は,最適化をしなければならない,という段階になるまではあまりないと思われます. また,C言語による様々な Python のモジュールが書かれていますが,初学者がその全容を見ていくのはなかなか困難です.

そこで今回は,Python C API を使ってバブルソートを書いていくことで,C言語による拡張がどのように行われているのかをつかんでみようと思い,やっていきました*1

Python C API とは

Python C API とは,PythonC言語で拡張したり,アプリケーションに Python を埋め込んだりするために使うことができるC言語APIです. 実体は CPython*2 の実装に使われているものとほぼ同等です.

バブルソートのモジュールを書く

なぜバブルソート

リストの操作,オブジェクトの大小比較あたりができて,そんなに難しくなさそうな題材だと思って取り上げました.

バブルソートを書く

まずはバブルソートの本体を書きます.アルゴリズムはいろんな教科書にも,Wikipedia にも載っているので,それを書き換えるだけ*3かと思いがちですが,CPython 特有の注意点が各所にあります.

#include <Python.h>

static PyObject* bubblesort (PyObject* self, PyObject* seq) {
  Py_ssize_t i, j, n;
  PyObject *a, *b, *list;
  int cmp;

  Py_INCREF(seq);
  list = PySequence_List(seq);
  Py_DECREF(seq);

  n = PyObject_Size(list);

  if (n < 0)
    return NULL; // error

  for (i = 1; i < n; ++i) for (j = 1; j < n - i + 1; ++j) {
    a = PyList_GetItem(list, j);
    Py_INCREF(a);
    b = PyList_GetItem(list, j - 1);
    Py_INCREF(b);

    cmp = PyObject_RichCompareBool(a, b, Py_LT); // a < b

    if (cmp == -1) { // error
      Py_DECREF(a);
      Py_DECREF(b);
      return NULL;
    }

    if (cmp == 1) {
      PyList_SetItem(list, j, b);
      PyList_SetItem(list, j - 1, a);
    } else {
      Py_DECREF(a);
      Py_DECREF(b);
    }
  }

  return list;
}

関数は static で,Python のオブジェクトを返すなら返り値は PyObject* で宣言する

後述しますが,モジュールの初期化を行う関数以外は全て static で宣言する必要があります. また,返り値に Python のオブジェクトを使うときは,そのポインタである PyObject* を返り値の型とする必要があります.

Python のオブジェクトへのポインタを受けるところは基本的に PyObject* で受ける

Python C API で返ってくるポインタは,基本的に全部 PyObject* へのポインタと思っていいと思います.list だろうが tuple だろうが,Python のオブジェクトなら全部 PyObject* です. C言語なのに動的型っぽくてマジか,となりますが,Python は動的型付け言語なので,まあそういうものと思うのがよいと思います.

他所のオブジェクトを参照しているときは Py_INCREF()Py_DECREF() で参照カウンタを適切に設定する

ここが一番のハマりどころだと思います*4

Python のメモリ管理は,参照カウントを用いて行われています. 詳しい説明は公式のドキュメントを読んでもらうとして,基本的には,他所のオブジェクトを使うときは Py_INCREF() で参照カウントを増やし,使い終わったら Py_DECREF() で減らす,でいいと思います. ただし,PyList_SetItem() では,セットする要素の参照は借用され,変更先の参照は破棄されます*5.そのため,その直後に参照カウントを減らすとセグメンテーション違反になります.

モジュールの初期化をする

static PyMethodDef MySortMethods[] = {
  {"bubblesort", (PyCFunction)bubblesort, METH_O, "Apply bubble sort to given list."},
  {NULL, NULL, 0, NULL}
};

static struct PyModuleDef mysortmodule = {
  PyModuleDef_HEAD_INIT,
  "mysort",
  NULL,
  -1,
  MySortMethods
};

PyMODINIT_FUNC PyInit_mysort (void) {
  return PyModule_Create(&mysortmodule);
}

上から順に見ていきます.

static PyMethodDef MySortMethods[] = {
  {"bubblesort", (PyCFunction)bubblesort, METH_O, "Apply bubble sort to given list."},
  {NULL, NULL, 0, NULL}
};

まず,モジュールの関数一覧を定義しています. 1行が1つの関数に対応しており,最後の行には関数一覧の終わりを示す行が必要となります.

PyMethodDef のメンバは次のようになっています.

メンバ Cの型 説明
ml_name char * 関数の名前を示す文字列.
ml_meth PyCFunction 関数ポインタ.関数を PyCFunction にキャストして渡す.
ml_flags int 関数の引数の指定方法を示すフラグ.
ml_doc char * 関数の docstring.

METH_O は,1つの PyObject * を引数に取る関数である*6,という意味のフラグです. 引数が複数個になる場合はもう少し変わってきます.

static struct PyModuleDef mysortmodule = {
  PyModuleDef_HEAD_INIT,
  "mysort",
  NULL,
  -1,
  MySortMethods
};

次に,モジュールの情報を定義します.

PyModuleDef のメンバは次のようになっています. 他にもメンバはありますが,今回は使っていません.詳しくは http://docs.python.jp/3/c-api/module.html#c.PyModuleDef を参照してください.

メンバ Cの型 説明
m_base PyModuleDef_Base 常に PyModuleDef_HEAD_INIT で初期化する.
m_name char * モジュールの名前を表す文字列.
m_doc char * モジュールの docstring.
m_size Py_ssize_t モジュールのメモリ領域におけるサイズ.
PyMODINIT_FUNC PyInit_mysort (void) {
  return PyModule_Create(&mysortmodule);
}

最後に,モジュールの初期化をする関数を定義します. この関数だけが非 static で定義されます.

今回は特別な処理などはなさそうなので,PyModule_Create() にモジュールの情報のアドレスを渡してあげ,モジュールの PyObject * を返してあげます.

これでモジュールを書くことができました.

モジュールをビルドする

せっかく書いたモジュールは,ビルドしないと Python からは使えません. ソースコードと同じディレクトリに setup.py を作り,このように記述します*7

from setuptools import setup, Extension

module = Extension(
    'mysort',
    sources=['mysortmodule.c']
)

setup(
    name='mysort',
    version='1.0',
    description='C API practice',
    ext_modules=[module],
)

そして,

$ python setup.py build

すれば,./build/ 以下にモジュールができています.

私の環境では,./build/lib.linux-x86_64-3.6/mysort.cpython-36m-x86_64-linux-gnu.so がビルドされたモジュールの本体でした. これを Python でインポートすれば C API で書いたモジュールを使うことができます.お疲れさまでした.

モジュールのテストを書いて走らせる

モジュールが正しく動作していることを確認するためにテストを書きたくなるのが世の常ですが,setup.py からも楽にテストを走らせることができます.

まず,setup.py を次のように書き換えます.

from setuptools import setup, Extension

module = Extension(
    'mysort',
    sources=['mysortmodule.c']
)

setup(
    name='mysort',
    version='1.0',
    description='C API practice',
    ext_modules=[module],
    test_suite='test.suite'
)

そして,test.py を書きます.内容はとりあえず次のようにしました.

import unittest
import mysort
import random


class MySortTest(unittest.TestCase):
    def test_bubblesort(self):
        l = [1, 6, 4, 2, 5, 3]

        self.assertEqual(
            [1, 2, 3, 4, 5, 6],
            mysort.bubblesort(l)
        )
        self.assertEqual(
            [1, 6, 4, 2, 5, 3],
            l
        )

    def test_bubblesort_from_tuple(self):
        tup = (1, 1, 4, 5, 1, 4)

        self.assertEqual(
            [1, 1, 1, 4, 4, 5],
            mysort.bubblesort(tup)
        )

    def test_bubblesort_very_long_list(self):
        l = list(range(10000))
        random.shuffle(l)

        self.assertEqual(
            list(range(10000)),
            mysort.bubblesort(l)
        )


def suite():
    suite = unittest.TestSuite()
    suite.addTests(unittest.makeSuite(MySortTest))
    return suite

そして,

$ python setup.py test

すると,モジュールをビルドした上で上記のテストコードを走らせてくれます.

おわりに

C言語を書いているはずなのに,動的型の言語を書いているような気持ちになりました.

github.com

今回書いたソースコードは上記のリポジトリに全部入ってます. このモジュールは PyPI にアップはしません*8

参考

*1:Python 風の文法のコードでC言語コンパイルできる Cython もありますが,今回はとりあえず C API のみを使うことにします.

*2:最も使われていると思われる,C言語による Python の実装.

*3:私はググりました.

*4:このため私はバブルソートを実装するのに2時間かけました.

*5:ここはやや怪しい.http://docs.python.jp/3/c-api/list.html#c.PyList_SetItem を参照.

*6:self もありますが,これはモジュールを表す変数です.

*7:setup.py は,Python のモジュールにおける Makefile 的な立ち位置にあるといってよいと思います.

*8:いくら C 拡張とはいえバブルソートでは実用的な速さは出ない.