私が歌川です

@utgwkk が書いている

YAPC::Kansai 2017 OSAKA 行った #yapckansai

下書きに残ってた.けっこう記憶から消えつつある.聞いたセッションの感想を書く.

メールフォームからメールを送る近代的な方法

/usr/sbin/sendmail とか SMTP とかの話だった.最近だと AWS もメール送信 API とかやってるらしい.

ビットコインを5分で試す

ブロックチェーンとは何なのか知れたのでよかった.Docker 出てきてモダンな感じがした.

2017年春のPerl

最近の Perl 事情あまり知らなかったけど,Perl6 だと演算子がめちゃくちゃ自由に作れるのすごい.

Webアプリケーションのキャッシュ戦略とそのパターン

ISUCON でやろうとしてたことだなーと思いながら聞いてた.キャッシュする前の本質的な改善ができるようになりたい.

GUEST: 木本 裕紀

あまり覚えていない.Perl 製の WAF の話だったと思う.

GUEST: スペシャルセッション

dankogai さんのライブコーディング.めちゃくちゃでかつてない感じになっていた.cgi-lib.pl はもう動かないと聞いて残念.

TRUNCATE user;に如何にして立ち向かうか

オペミスによる DB データ消失の対策みたいな話だったと思う.

はてなシステムの考古学

はてなは Perl の企業という印象が強いけど,ある時点での決断によるものだったのだと分かった.最近はいろいろ使ってるみたいだ.

LT

Acme モジュールの話がよかった.

Keynote: 竹迫 良範

60分あってけっこう長かったけど,マインスイーパを自動化するところと,USB の自動再生でマウスを止めるところが一番よかった.

総合

どの話も(ちゃんと聞いてたのは)面白かったと思う.Perl の会だと思って来ているけど,Perl 関係ない話もどんどん出てきて身近な感じがした.休憩時間なしでセッション詰めるスケジュールはけっこう大変そうな感じがした.

Fitbit Charge 2 買った

Fitbit Charge2 フィットネストラッカー Black Lサイズ [日本正規品] FB407SBKL-JPN

Fitbit Charge2 フィットネストラッカー Black Lサイズ [日本正規品] FB407SBKL-JPN

3月のやっていきです.

きっかけ

睡眠の質を記録したいと思った.サークル内でもいろんな人が使ってるしということで買った.

おもしろそうだけど自転車のほうが乗ってそうな気もしていて,でも最近はそうでもない気もしているが,まあはいがんばります.

https://www.fitbit.com/user/5JPDF4

どんどん歩きましょう.はやく健康になりたい.

Python の map とfor内包表記(リスト内包表記)は結局どっちが速い?

tl;dr

  • map が遅いとされるのは関数呼び出しの差があったため
  • 現在では(list にこだわらなければ) map(2017/3/10 11:00追記)イテレータを生成するときは 圧倒的に速い
  • 総合的に見ると,式のみのときはfor内包表記が速く,関数のときは map が速い様子か

追記が増えてきたので,追記部分も読んでもらえるとよさそう.

はじめに

Python の map とfor内包表記(リスト内包表記)は結局どっちが速い?」というのは,Python 使いなら誰しもが抱く疑問かと思われる.そういう記事もいっぱいある.

そこで,今回はバイトコードと実行時間を見ることで,どちらが速いのかを検証してみた.

Python2.7.12 の場合

次のようなコードを用意した.どちらも0から9までの自然数の2乗のリストを返す.

初めに逆アセンブルされたバイトコードを出力し,次に timeit で実行時間を計測する.

from __future__ import print_function
from timeit import timeit
from dis import dis

def b():
    return [i ** 2 for i in xrange(10)]

def d():
    return map(lambda i: i ** 2, xrange(10))

print('==========b=========')
dis(b)
print('==========d=========')
dis(d)
print('========timeit======')
print('b() ->', timeit('b()', 'def b(): return [i ** 2 for i in xrange(10)]'))
print('d() ->', timeit('d()', 'def d(): return map(lambda i: i ** 2, xrange(10))'))

結果は次の通り.

==========b=========
  6           0 BUILD_LIST               0
              3 LOAD_GLOBAL              0 (xrange)
              6 LOAD_CONST               1 (10)
              9 CALL_FUNCTION            1
             12 GET_ITER
        >>   13 FOR_ITER                16 (to 32)
             16 STORE_FAST               0 (i)
             19 LOAD_FAST                0 (i)
             22 LOAD_CONST               2 (2)
             25 BINARY_POWER
             26 LIST_APPEND              2
             29 JUMP_ABSOLUTE           13
        >>   32 RETURN_VALUE
==========d=========
  9           0 LOAD_GLOBAL              0 (map)
              3 LOAD_CONST               1 (<code object <lambda> at 0x7f1406ac2cb0, file "hoge2.py", line 9>)
              6 MAKE_FUNCTION            0
              9 LOAD_GLOBAL              1 (xrange)
             12 LOAD_CONST               2 (10)
             15 CALL_FUNCTION            1
             18 CALL_FUNCTION            2
             21 RETURN_VALUE
========timeit======
b() -> 1.17981290817
d() -> 1.73737597466

結果から,バイトコードはfor内包の方が長いこと,一方で実行時間は map のほうが1.5倍ほどかかっていることが分かる.

for内包では計算内容はバイトコードに直接展開されているのに対して, map ではいったん関数にしてからそれを評価するので,そのオーバーヘッドが大きいのだろう.

ではこの場合はどうか? for内包表記でも関数を呼び出すようにしてみた.

from __future__ import print_function
from timeit import timeit
from dis import dis

def b():
    f = lambda x: x ** 2
    return [f(i) for i in xrange(10)]

def d():
    return map(lambda i: i ** 2, xrange(10))

print('==========b=========')
dis(b)
print('==========d=========')
dis(d)
print('========timeit======')
print('b() ->', timeit('b()', 'def b(): f = lambda x: x ** 2; return [f(i) for i in xrange(10)]'))
print('d() ->', timeit('d()', 'def d(): return map(lambda i: i ** 2, xrange(10))'))

結果は次の通り.

==========b=========
  6           0 LOAD_CONST               1 (<code object <lambda> at 0x7f61c40fc930, file "hoge2.py", line 6>)
              3 MAKE_FUNCTION            0
              6 STORE_FAST               0 (f)

  7           9 BUILD_LIST               0
             12 LOAD_GLOBAL              0 (xrange)
             15 LOAD_CONST               2 (10)
             18 CALL_FUNCTION            1
             21 GET_ITER
        >>   22 FOR_ITER                18 (to 43)
             25 STORE_FAST               1 (i)
             28 LOAD_FAST                0 (f)
             31 LOAD_FAST                1 (i)
             34 CALL_FUNCTION            1
             37 LIST_APPEND              2
             40 JUMP_ABSOLUTE           22
        >>   43 RETURN_VALUE
==========d=========
 10           0 LOAD_GLOBAL              0 (map)
              3 LOAD_CONST               1 (<code object <lambda> at 0x7f61c40a37b0, file "hoge2.py", line 10>)
              6 MAKE_FUNCTION            0
              9 LOAD_GLOBAL              1 (xrange)
             12 LOAD_CONST               2 (10)
             15 CALL_FUNCTION            1
             18 CALL_FUNCTION            2
             21 RETURN_VALUE
========timeit======
b() -> 2.16676402092
d() -> 1.90643000603

なんと,map のほうが速くなった.

これはmapと内包表記の差ではなく、おそらく関数呼び出しのコストの差が大きいのでしょう。

mapと内包表記の速度の差について - 素数好きの最高技術責任者のブログ

map は決して遅くない.ただ,条件が不利になったときに遅いのである.

Python 3.6.0 の場合

list を返す場合

次に Python 3.6.0 の場合を考える.map の返り値はイテレータになったので,list が欲しい場合はたいていはこのようにして変換するとよい.

from timeit import timeit
from dis import dis

def b():
    return [i ** 2 for i in range(10)]

def d():
    return list(map(lambda i: i ** 2, range(10)))

print('==========b=========')
dis(b)
print('==========d=========')
dis(d)
print('========timeit======')
print('b() ->', timeit('b()', globals=globals()))
print('d() ->', timeit('d()', globals=globals()))

結果は次の通り.

==========b=========
  5           0 LOAD_CONST               1 (<code object <listcomp> at 0x7fa8e6486ae0, file "hoge.py", line 5>)
              2 LOAD_CONST               2 ('b.<locals>.<listcomp>')
              4 MAKE_FUNCTION            0
              6 LOAD_GLOBAL              0 (range)
              8 LOAD_CONST               3 (10)
             10 CALL_FUNCTION            1
             12 GET_ITER
             14 CALL_FUNCTION            1
             16 RETURN_VALUE
==========d=========
  8           0 LOAD_GLOBAL              0 (list)
              2 LOAD_GLOBAL              1 (map)
              4 LOAD_CONST               1 (<code object <lambda> at 0x7fa8e6449c90, file "hoge.py", line 8>)
              6 LOAD_CONST               2 ('d.<locals>.<lambda>')
              8 MAKE_FUNCTION            0
             10 LOAD_GLOBAL              2 (range)
             12 LOAD_CONST               3 (10)
             14 CALL_FUNCTION            1
             16 CALL_FUNCTION            2
             18 CALL_FUNCTION            1
             20 RETURN_VALUE
========timeit======
b() -> 4.135329754004488
d() -> 5.330336746992543

map のほうが1.3倍ほど遅いことが分かった.一方,バイトコードの長さはあまり変わらない.

ここで,条件を少し変えてみる.

イテレータを返す場合

次のコードを考える.いずれもイテレータを返すように変更されている.

from timeit import timeit
from dis import dis

def b():
    return iter([i ** 2 for i in range(10)])

def d():
    return map(lambda i: i ** 2, range(10))

print('==========b=========')
dis(b)
print('==========d=========')
dis(d)
print('========timeit======')
print('b() ->', timeit('b()', globals=globals()))
print('d() ->', timeit('d()', globals=globals()))

結果は次の通り.

==========b=========
  5           0 LOAD_GLOBAL              0 (iter)
              2 LOAD_CONST               1 (<code object <listcomp> at 0x7f1db4912ae0, file "hoge.py", line 5>)
              4 LOAD_CONST               2 ('b.<locals>.<listcomp>')
              6 MAKE_FUNCTION            0
              8 LOAD_GLOBAL              1 (range)
             10 LOAD_CONST               3 (10)
             12 CALL_FUNCTION            1
             14 GET_ITER
             16 CALL_FUNCTION            1
             18 CALL_FUNCTION            1
             20 RETURN_VALUE
==========d=========
  8           0 LOAD_GLOBAL              0 (map)
              2 LOAD_CONST               1 (<code object <lambda> at 0x7f1db48d5c90, file "hoge.py", line 8>)
              4 LOAD_CONST               2 ('d.<locals>.<lambda>')
              6 MAKE_FUNCTION            0
              8 LOAD_GLOBAL              1 (range)
             10 LOAD_CONST               3 (10)
             12 CALL_FUNCTION            1
             14 CALL_FUNCTION            2
             16 RETURN_VALUE
========timeit======
b() -> 4.343064354005037
d() -> 0.6455312269972637

なんと,この場合は map のほうが6倍以上速いという結果になった.これはどういうことか.原因はいくつか考えられる.

イテレータから list への変換が遅い

そもそも list へ変換する場合と比べて速すぎるので,list への変換が重い処理であることは間違いないだろう.

for内包表記でも一時関数を作るようになった

b()バイトコードのうち,i ** 2 を計算する部分に注目してみよう.

Python 2.7.12 ではこうだった.

19 LOAD_FAST                0 (i)
22 LOAD_CONST               2 (2)
25 BINARY_POWER

一方で Python 3.6.0 ではこうだ.

2 LOAD_CONST               1 (<code object <listcomp> at 0x7ff334fd4ae0, file "hoge.py", line 5>)
4 LOAD_CONST               2 ('b.<locals>.<listcomp>')
6 MAKE_FUNCTION            0
16 CALL_FUNCTION           1

for内包表記の方にも MAKE_FUNCTION という命令が登場していることが分かる.一時関数を作って,それを呼ぶようにしているのだろう.

追記(2017/3/9 17:10)

map も遅延評価されているのは知らなかったです.確かにそれならイテレータだけ作る場合は圧倒的に速そう.

id:methane さんにも同じ指摘をいただきました.

追記(2017/3/9 17:22)

以下のコードを Python 3.6.0 で動かす.

from timeit import timeit
from dis import dis

def b():
    for a in iter([i ** 2 for i in range(10)]):
        continue

def d():
    for a in map(lambda i: i ** 2, range(10)):
        continue

print('==========b=========')
dis(b)
print('==========d=========')
dis(d)
print('========timeit======')
print('b() ->', timeit('b()', globals=globals()))
print('d() ->', timeit('d()', globals=globals()))
==========b=========
  5           0 SETUP_LOOP              32 (to 34)
              2 LOAD_GLOBAL              0 (iter)
              4 LOAD_CONST               1 (<code object <listcomp> at 0x7f5c2c97aae0, file "hoge3.py", line 5>)
              6 LOAD_CONST               2 ('b.<locals>.<listcomp>')
              8 MAKE_FUNCTION            0
             10 LOAD_GLOBAL              1 (range)
             12 LOAD_CONST               3 (10)
             14 CALL_FUNCTION            1
             16 GET_ITER
             18 CALL_FUNCTION            1
             20 CALL_FUNCTION            1
             22 GET_ITER
        >>   24 FOR_ITER                 6 (to 32)
             26 STORE_FAST               0 (a)

  6          28 JUMP_ABSOLUTE           24
             30 JUMP_ABSOLUTE           24
        >>   32 POP_BLOCK
        >>   34 LOAD_CONST               0 (None)
             36 RETURN_VALUE
==========d=========
 11           0 SETUP_LOOP              28 (to 30)
              2 LOAD_GLOBAL              0 (map)
              4 LOAD_CONST               1 (<code object <lambda> at 0x7f5c2c93dc90, file "hoge3.py", line 11>)
              6 LOAD_CONST               2 ('d.<locals>.<lambda>')
              8 MAKE_FUNCTION            0
             10 LOAD_GLOBAL              1 (range)
             12 LOAD_CONST               3 (10)
             14 CALL_FUNCTION            1
             16 CALL_FUNCTION            2
             18 GET_ITER
        >>   20 FOR_ITER                 6 (to 28)
             22 STORE_FAST               0 (a)

 12          24 JUMP_ABSOLUTE           20
             26 JUMP_ABSOLUTE           20
        >>   28 POP_BLOCK
        >>   30 LOAD_CONST               0 (None)
             32 RETURN_VALUE
========timeit======
b() -> 4.407405980993644
d() -> 4.945474245003425

for文で回すと map のほうが遅いという結果になった. 「あらゆる場面で map のほうが速い」というのは言えなさそうだ.

追記(2017/3/10 10:46)

from __future__ import print_function
from dis import dis
from timeit import timeit

def one():
    for b in [abs(x) for x in range(-5, 5)]:
        continue

def two():
    for b in map(abs, range(-5, 5)):
        continue

print("===========one===========")
dis(one)
print("===========two===========")
dis(two)
print('for comprehension:', timeit('for b in [abs(x) for x in range(-5, 5)]: continue'))
print('map:', timeit('for b in map(abs, range(-5, 5)): continue'))
===========one===========
  6           0 SETUP_LOOP              30 (to 32)
              2 LOAD_CONST               1 (<code object <listcomp> at 0x7f3fb9008ae0, file "fuga.py", line 6>)
              4 LOAD_CONST               2 ('one.<locals>.<listcomp>')
              6 MAKE_FUNCTION            0
              8 LOAD_GLOBAL              0 (range)
             10 LOAD_CONST               4 (-5)
             12 LOAD_CONST               3 (5)
             14 CALL_FUNCTION            2
             16 GET_ITER
             18 CALL_FUNCTION            1
             20 GET_ITER
        >>   22 FOR_ITER                 6 (to 30)
             24 STORE_FAST               0 (b)

  7          26 JUMP_ABSOLUTE           22
             28 JUMP_ABSOLUTE           22
        >>   30 POP_BLOCK
        >>   32 LOAD_CONST               0 (None)
             34 RETURN_VALUE
===========two===========
 10           0 SETUP_LOOP              26 (to 28)
              2 LOAD_GLOBAL              0 (map)
              4 LOAD_GLOBAL              1 (abs)
              6 LOAD_GLOBAL              2 (range)
              8 LOAD_CONST               2 (-5)
             10 LOAD_CONST               1 (5)
             12 CALL_FUNCTION            2
             14 CALL_FUNCTION            2
             16 GET_ITER
        >>   18 FOR_ITER                 6 (to 26)
             20 STORE_FAST               0 (b)

 11          22 JUMP_ABSOLUTE           18
             24 JUMP_ABSOLUTE           18
        >>   26 POP_BLOCK
        >>   28 LOAD_CONST               0 (None)
             30 RETURN_VALUE
for comprehension: 1.5314300169993658
map: 0.9652115959906951

これは Python 3.6.0 での結果だが,確かに map の方が速かった(Python 2.7.12 でも map が勝っていた).

まとめ

  • map よりfor内包のほうが速い」とは一概に言えない
    • 追記(2017/3/9 17:22) どちらも一概に言えない気がしてきた……
  • (2017/3/9 17:04追記)map では要素が遅延評価されているので高速になっていた
  • この速度差は関数呼び出しのコストの差(2017/3/9 17:04追記)もあった
    • Python3系ではそのコストの差も消えた
  • list に変換する処理はかなり重い
  • そもそも list を作るのが重い?

関数を呼ばない計算を適用した結果のリストが欲しいときはfor内包を使い,関数を呼ぶ場合や,(Python3系では)イテレータだけが欲しいときは map を使う,というふうにするのがよいと思われる.

いつの日にか map は極限まで最適化されていたのだろう.map は遅延評価イテレータに化けていた.それは速いわけだ.

参考

3月のやっていき

utgwkk.hateblo.jp

utgwkk.hateblo.jp

野菜350g

Amazon 定期おトク便で野菜ジュースが届くようになったので,毎日1本飲んでる.

コーヒーを断つ

いつまで続くかは不明.代わりに水を飲んでる.水は水の味しかしないのでお得感少ないけど,体調がめちゃくちゃになるリスクは少なそう.

プランク

昨日から再開した.デレステの1曲分*1を2回やっている.けっこう長いのでしんどい.

皮膚科

皮膚科に行って薬をもらってきた.使いはじめてからだいぶ皮膚は落ち着いた気がする.プラセボでもそれはそれでお得.

まとめ

Nintendo Switch 届いてしまう.3月もやっていきましょう.

*1:だいたい2分.

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 拡張とはいえバブルソートでは実用的な速さは出ない.

眠たさをカフェインでごまかすのは悪い文明であるということにいい加減に気づかないといけないなあーと思っています.

前日の睡眠時間が狂っていて*1,朝9時ぐらいにめちゃくちゃ眠たくなってインスタントコーヒーを投入したところ,落ち着きがなくなったような変な具合になり,いろいろ断念した.これを書いている今はまぶたが重たい.血圧測ったところ普段よりかなり低かった.

野菜ジュース届いたし,皮膚科にも行けたので,ここからなんとか持ち直したい.

*1:21時に寝て夜3時ぐらいに目が覚めて,ずっと起きていた