Cythonによるパフォーマンス向上

Cython ―Cとの融合によるPythonの高速化

Cython ―Cとの融合によるPythonの高速化

Pythonのような書きやすいインタープリタ型言語で書き始めて結果を取得できるところまでいくと、その後にはパフォーマンスを上げるためにC++で書きなおそうか、と思ったりする。かといって最初からC++で書いていては、きちんと動くものを書き切るのに間違いなく時間がかかるし、とにかくまず結果が欲しいときには取るべき有効な手段ではないと思う。

そこでCythonの出番。Pythonのコードはそのままで有効なCythonのコードでもあるので、cythonをインストールして拡張子pyxに変更し、import pyximport; pyximport.install()をつけるだけで速度改善の効果が望める。もちろん、静的な型指定などのチューニングを施すことによって、Pythonのセマンティクスを保ったまま、よりCに近い速度を達成することができる。これだけ簡単に導入できるので、もしCPUバウンドな処理であることが分かっているなら、Cythonを使わない手はないだろう。

どのような場合にどの程度の速度改善が望めるか、以下のようにPythonのコードを実行した結果と、PythonのコードをそのままCythonとしてコンパイルして実行した結果、Cythonとして書き直したコードを実行した結果、を比較した。プログラムの内容は、単にソートを実施しているだけだが、再帰呼び出しの有り・無し、でそれぞれプログラムを変えている。Cythonでは、stdlib.hqsortを使っている。

再帰呼び出し有り、では関数呼び出しのオーバーヘッドが大きくなるため、PythonのコードをそのままCythonとしてコンパイルして実行した結果、が、Pythonのコードを実行した結果に比べて、速度改善率が高い。

再帰呼び出し無し、では、ソートするリストの長さを1000倍しており、Cythonのコードの速度改善率がかなり高い。

このようにある特定のモジュールをCython化するだけでも、かなり速度改善が望めることが分かる。Cそのもののコードを書かずにPythonスクリプトの書きやすさを保ったままパフォーマンス向上が可能となる。あとは、プロファイラーを使ってCython化すべき箇所を特定する作業が必要になるだろう。

再帰呼び出し有り

$ cat pyqsort.py 
def pyqsort(x):
    if len(x)==1:
        return
    array = sorted(x)
    pyqsort(x[:-1])
$ cat cyqsort.pyx 
def cyqsort(x):
    if len(x)==1:
        return
    array = sorted(x)
    cyqsort(x[:-1])
$ cat cqsort.pyx 
cdef extern from "stdlib.h":
    void qsort(void *array,size_t count,size_t size,int (*compare)(const void*,const void*))
    void *malloc(size_t size)
    void free(void *ptr)

def cqsort(list x):
    cdef:
        int *array
        int i, N
    
    N = len(x)
    if N==1:
        return
    array = <int*>malloc(sizeof(int) * N)
    if array == NULL:
        raise MemoryError("Unable to allocate array.")

    for i in range(N):
        array[i] = x[i]
        
    qsort(<void*>array, <size_t>N, sizeof(int), int_compare)
   
    free(array)

    cqsort(x[:-1])

cdef int int_compare(const void *a, const void *b):
    cdef int ia, ib
    ia = (<int*>a)[0]
    ib = (<int*>b)[0]
    return ia-ib
$ ipython --no-banner

In [1]: from pyqsort import pyqsort

In [2]: from random import shuffle

In [3]: x=list(range(100))

In [4]: shuffle(x)

In [5]: %timeit pyqsort(x)
1000 loops, best of 3: 642 µs per loop

In [6]: import pyximport; pyximport.install()
Out[6]: (None, <pyximport.pyximport.PyxImporter at 0x106dc1eb8>)

In [7]: from cyqsort import cyqsort

In [8]: %timeit cyqsort(x)
1000 loops, best of 3: 555 µs per loop

In [9]: from cqsort import cqsort

In [10]: %timeit cqsort(x)
1000 loops, best of 3: 349 µs per loop

In [11]: 

再帰呼び出し無し

$ cat pyqsort_once.py 
def pyqsort(x):
    array = sorted(x)
$ cat cyqsort_once.pyx
def cyqsort(x):
    array = sorted(x)
$ cat cqsort_once.pyx
cdef extern from "stdlib.h":
    void qsort(void *array,size_t count,size_t size,int (*compare)(const void*,const void*))
    void *malloc(size_t size)
    void free(void *ptr)

def cqsort(list x):
    cdef:
        int *array
        int i, N
    
    N = len(x)
    if N==1:
        return
    array = <int*>malloc(sizeof(int) * N)
    if array == NULL:
        raise MemoryError("Unable to allocate array.")

    for i in range(N):
        array[i] = x[i]
        
    qsort(<void*>array, <size_t>N, sizeof(int), int_compare)
   
    free(array)

cdef int int_compare(const void *a, const void *b):
    cdef int ia, ib
    ia = (<int*>a)[0]
    ib = (<int*>b)[0]
    return ia-ib
$ ipython --no-banner

In [1]: from pyqsort_once import pyqsort

In [2]: from random import shuffle

In [3]: x=list(range(1000000))

In [4]: shuffle(x)

In [5]: %timeit pyqsort(x)
1 loop, best of 3: 760 ms per loop

In [6]: import pyximport; pyximport.install()
Out[6]: (None, <pyximport.pyximport.PyxImporter at 0x10c9bbc88>)

In [7]: from cyqsort_once import cyqsort

In [8]: %timeit cyqsort(x)
1 loop, best of 3: 759 ms per loop

In [9]: from cqsort_once import cqsort

In [10]: %timeit cqsort(x)
1 loop, best of 3: 193 ms per loop

In [11]: 

ループで呼び出し

$ ipython --no-banner

In [1]: from pyqsort_once import pyqsort

In [2]: from random import shuffle

In [3]: x=list(range(1000))

In [4]: shuffle(x)

In [5]: %timeit for i in range(1000): pyqsort(x)
1 loop, best of 3: 218 ms per loop

In [6]: import pyximport; pyximport.install()
Out[6]: (None, <pyximport.pyximport.PyxImporter at 0x10dd583c8>)

In [7]: from cyqsort_once import cyqsort

In [8]: %timeit for i in range(1000): cyqsort(x)
1 loop, best of 3: 218 ms per loop

In [9]: from cqsort_once import cqsort

In [10]: %timeit for i in range(1000): cqsort(x)
10 loops, best of 3: 84.8 ms per loop

In [11]: 

Pythonの正規表現でバックスラッシュを含む文字列とマッチする場合

Pythonのreモジュールでsearchなどの関数に正規表現を与える場合、rubyperlなどのように正規表現を直接与えることができず、文字列として与える必要があるため、WindowsのPath区切りにマッチさせたいときなどに正規表現内にバックスラッシュを使いたい場合、一つのバックスラッシュに対して"\\\\"とする必要がある。rプレフィックスを用いることで、文字列としてのエスケープは行われなくなるので、正規表現としての一つのバックスラッシュをr"\\"と書ける。 ただし文字列の最後にr"\"とは書けないことにも注意が必要。

reモジュールのsearch関数の第1引数は正規表現、第2引数はマッチ対象となる文字列だ。

>>> import re
>>> re.search("abc","abc")
<_sre.SRE_Match object; span=(0, 3), match='abc'>
>>> re.search(r"abc\\abc","abc\\abc")
<_sre.SRE_Match object; span=(0, 7), match='abc\\abc'>
>>> re.search("abc\\\\abc","abc\\abc")
<_sre.SRE_Match object; span=(0, 7), match='abc\\abc'>
>>> re.search(r"abc\\\\abc",r"abc\\abc")
<_sre.SRE_Match object; span=(0, 8), match='abc\\\\abc'>
>>> re.search(r"abc\\abc",r"abc\abc")
<_sre.SRE_Match object; span=(0, 7), match='abc\\abc'>
>>> re.search(r"abc\\abc\\",r"abc\abc\")
  File "<stdin>", line 1
    re.search(r"abc\\abc\\",r"abc\abc\")
                                       ^
SyntaxError: EOL while scanning string literal
>>> re.search(r"abc\\abc\\","abc\\abc\\")
<_sre.SRE_Match object; span=(0, 8), match='abc\\abc\\'>

以下のように正規表現の文字列として"\\"を与えてしまうと、文字列内でエスケープした結果として\正規表現として与えられてしまいエラーとなる。\は単独では正規表現として有効ではない。

>>> re.search("abc\\","abc\\")
Traceback (most recent call last):
  File "/usr/local/Cellar/python3/3.4.3_2/Frameworks/Python.framework/Versions/3.4/lib/python3.4/sre_parse.py", line 206, in __next
    c = self.string[self.index + 1]
IndexError: string index out of range
...

>>> re.search("\\","")
Traceback (most recent call last):
  File "/usr/local/Cellar/python3/3.4.3_2/Frameworks/Python.framework/Versions/3.4/lib/python3.4/sre_parse.py", line 206, in __next
    c = self.string[self.index + 1]
IndexError: string index out of range
...

バックスラッシュを含む文字列が変数に代入されており、それを正規表現として扱いたい場合、文字列中のバックスペースは二回エスケープされることになるので、予め"\\\\"とするか、あるいはrプレフィックスを与えてr"\\"としておく必要がある。

>>> a="abc\\\\abc"
>>> a
'abc\\\\abc'
>>> re.search(a,"abc\\abc")
<_sre.SRE_Match object; span=(0, 7), match='abc\\abc'>
>>> a=r"abc\\abc"
>>> a
'abc\\\\abc'
>>> re.search(a,"abc\\abc")
<_sre.SRE_Match object; span=(0, 7), match='abc\\abc'>

以下は誤ったやり方になる。上記の通り、rプレフィックスは文字列としてのエスケープを行わない、という指示になるので、新たにrプレフィックスを付けた文字列に変数をフォーマットしても期待通りには動かない。また、%rでフォーマットし直してもうまくいかない。どうにかして(正規表現として使われることを意識してしない)元の変数をsearchの第1引数に与えようと、replevalを試してみてもうまくいかない。

>>> a="abc\\abc"
>>> re.search(a,"abc\\abc")
>>> re.search(a,"abc\abc")
<_sre.SRE_Match object; span=(0, 6), match='abc\x07bc'>
>>> r"{0}".format(a)
'abc\\abc'
>>> "%r"%a
"'abc\\\\abc'"
>>> re.search("%r"%a,"abc\\abc")
>>>
>>>repr(a)
"'abc\\\\abc'"
>>> eval(repr(a))
'abc\\abc'

そのため、以下のようにsub関数による置換が必要になるかもしれない。

>>> a="abc\\abc"
>>> a=re.sub(r"\\",r"\\\\",a)
>>> a
'abc\\\\abc'
>>> re.search(a,"abc\\abc")
<_sre.SRE_Match object; span=(0, 7), match='abc\\abc'>

stroll(nicer walk)のC++バージョンを書いた

ディレクトリを幅優先探索で掘っていき、ディレクトリやファイルに対する条件を適用し、それらを与えた条件(関数)によってソートし、見つけたファイルパスを返すライブラリを書いた。ちょうどPythonのos.walkを高機能にしたもので、所望のファイルだけを取り出したいときに使える。

boost不要で、C++11/C++14も不要なので、古いバージョンのg++(clang)でもコンパイルできる(はず)。Cでファイル処理を書くことはほとんど無いはずなので、C++で使えれば良いかなと。CとC++がごっちゃになっていてなんとなく居心地が悪いけどこんなもんなのだろうか。ちなみに、directory_iterator#incude <filesystem>が必要でMac(clang)だとコンパイルできないようなので、Cを使うことにした。

正規表現マッチをする際に、matchとsearchの違いを忘れてしまいがち。matchは先頭からマッチするが、searchはどこでもマッチする。

gist.github.com

$ cat main.cpp
#include <iostream>
#include "stroll.cpp"

using namespace std;
using namespace stroll;

void ex1(){
    cout<<"=== ex1 (mtime) ==="<<endl;
    Stroll stroll = Stroll("basedir");
    stroll.set_sortby(sortByMtime);

    while(stroll.read()){
        cout<< stroll.get() << endl;
    }
}

void ex2(){
    cout<<"=== ex2 (mtime/reverse) ==="<<endl;
    Stroll stroll = Stroll("basedir");
    stroll.set_sortby(sortByMtimeDesc);

    while(stroll.read()){
        cout<< stroll.get() << endl;
    }
}

void ex3(){
    cout<<"=== ex3 (no_file) ==="<<endl;
    vstr no_file;
    no_file.push_back("1$");
    no_file.push_back("2$");

    Stroll stroll = Stroll("basedir");
    stroll.set_no_file(no_file);

    while(stroll.read()){
        cout<< stroll.get() << endl;
    }
}

void ex4(){
    cout<<"=== ex4 (yes_path) ==="<<endl;
    vstr yes_path;
    yes_path.push_back("1$");

    Stroll stroll = Stroll("basedir");
    stroll.set_yes_path(yes_path);

    while(stroll.read()){
        cout<< stroll.get() << endl;
    }
}

void ex5(){
    cout<<"=== ex5 (no_root/len/reverse) ==="<<endl;
    vstr no_root;
    no_root.push_back("2$");

    Stroll stroll = Stroll("basedir");
    stroll.set_no_root(no_root);
    stroll.set_sortby(sortByLenDesc);

    while(stroll.read()){
        cout<< stroll.get() << endl;
    }
}

void ex6(){
    cout<<"=== ex6 (yes_file/yes_root) ==="<<endl;
    vstr yes_file;
    yes_file.push_back("2$");

    vstr yes_root;
    yes_root.push_back("2$");

    Stroll stroll = Stroll("basedir");
    stroll.set_yes_file(yes_file);
    stroll.set_yes_root(yes_root);

    while(stroll.read()){
        cout<< stroll.get() << endl;
    }
}

int main(int argc, char* argv[]){

    ex1();
    ex2();
    ex3();
    ex4();
    ex5();
    ex6();

    return 0;
}
$ cat Makefile 

main:main.o
    g++ -L./ -lstroll -o main main.o

main.o:libstroll.a
    g++ -c main.cpp

libstroll.a:stroll.o
    ar r libstroll.a stroll.o

stroll.o:stroll.cpp
    g++ -Wall -O2 -c stroll.cpp

.PHONY: clean
clean:
    rm -f libstroll.a stroll.o main
$ ./main 
=== ex1 (mtime) ===
basedir/file0
basedir/dir1/file11
basedir/dir1/file12
basedir/dir1/file13
basedir/dir1/file111
basedir/dir1/file1111
basedir/dir2/file21
basedir/dir2/file22
basedir/dir2/file23
basedir/dir2/file222
basedir/dir2/file2222
basedir/dir3/file31
basedir/dir3/file32
basedir/dir3/file33
basedir/dir3/file333
basedir/dir3/file3333
=== ex2 (mtime/reverse) ===
basedir/file0
basedir/dir3/file3333
basedir/dir3/file333
basedir/dir3/file33
basedir/dir3/file32
basedir/dir3/file31
basedir/dir2/file2222
basedir/dir2/file222
basedir/dir2/file23
basedir/dir2/file21
basedir/dir2/file22
basedir/dir1/file1111
basedir/dir1/file111
basedir/dir1/file13
basedir/dir1/file12
basedir/dir1/file11
=== ex3 (no_file) ===
basedir/file0
basedir/dir1/file13
basedir/dir2/file23
basedir/dir3/file33
basedir/dir3/file333
basedir/dir3/file3333
=== ex4 (yes_path) ===
basedir/file0
basedir/dir1/file11
basedir/dir1/file111
basedir/dir1/file1111
basedir/dir1/file12
basedir/dir1/file13
=== ex5 (no_root/len/reverse) ===
basedir/file0
basedir/dir1/file1111
basedir/dir1/file111
basedir/dir1/file11
basedir/dir1/file12
basedir/dir1/file13
basedir/dir3/file3333
basedir/dir3/file333
basedir/dir3/file31
basedir/dir3/file32
basedir/dir3/file33
=== ex6 (yes_file/yes_root) ===
basedir/dir2/file22
basedir/dir2/file222
basedir/dir2/file2222

os.walkを便利に使える関数strollを書いた

  • リスト指定と正規表現マッチに対応し、ディレクトリパスとファイル名を返すようにして、関数名をnicewalkからstroll*1に変更(短い単語の方が良かった)

  • ディレクトリの扱いを明確化。rootはカレントディレクトリのディレクトリ名で、pathはカレントディレクトリのパス。ignorecaseを追加。

  • Python 3.5以上ではglobにrecursiveオプションが付いたため、"**"とrecursive=Trueを与えることで再帰的に辿ることができるようになった。そのため、以下はPython3.4以下が対象。

os.walk()はディレクトリを再帰的に辿りながら、ファイルを読み込んだり書き込む処理をする際に非常に便利だ。globだとディレクトリを再帰的に辿ることができず、ディレクトリ階層を意識しないといけない*2し、ディレクトリとファイルの違いをワイルドカードで示す必要があるので柔軟さに欠ける。またos.walk()では、取得したディレクトリやファイルのリストに対して、インプレースでの変更が可能になっているため、任意の条件で自由にソートやフィルタを行うことができる。そこで、各ディレクトリ・ファイルに対する条件を簡単に盛り込めるstrollを書いた。

reduceのこの使い方は好きで、リストをループでチェックするよりワンラインで書ける方が見やすくて良い。一度慣れてしまえば、それほど考えこまなくても流用できるので、いろんなところで多用している。リストが空の場合には、reduceに与える初期値がそのまま判定結果になってしまうが、それが嫌ならリストを三項演算で判定して初期値を与えれば良い。

gist.github.com

$ python --version
Python 3.4.3

$ cat main.py 
from stroll import stroll
import os

print()
print("=== ex1 (mtime) ===")
for filepath in stroll("basedir",sortby=os.path.getmtime):
    print( filepath )

print()
print("=== ex2 (mtime/reverse) ===")
for filepath in stroll("basedir",sortby=os.path.getmtime,reverse=True):
    print( filepath )

print()
print("=== ex3 (no_file) ===")
for filepath in stroll("basedir",no_file=["1$","2$"],):
    print( filepath )

print()
print("=== ex4 (yes_path) ===")
for filepath in stroll("basedir",yes_path=["1$"],):
    print( filepath )

print()
print("=== ex5 (no_root/len/reverse) ===")
for filepath in stroll("basedir",no_root=["2$"],sortby=len,reverse=True):
    print( filepath )

print()
print("=== ex6 (yes_file/yes_root) ===")
for filepath in stroll("basedir",yes_file=["2$"],yes_root=["2$"]):
    print( file path )
$ find basedir
basedir
basedir/dir1
basedir/dir1/file11
basedir/dir1/file111
basedir/dir1/file1111
basedir/dir1/file12
basedir/dir1/file13
basedir/dir2
basedir/dir2/file21
basedir/dir2/file22
basedir/dir2/file222
basedir/dir2/file2222
basedir/dir2/file23
basedir/dir3
basedir/dir3/file31
basedir/dir3/file32
basedir/dir3/file33
basedir/dir3/file333
basedir/dir3/file3333
basedir/file0

$ ls -Rlrt --time-style=+"%m/%d_%H:%M:%S" basedir/ | awk '{print $5 " " $6 " " $7}'
  
0 02/09_21:24:50 file0
238 02/09_21:25:29 dir1
238 02/09_21:25:33 dir2
238 02/09_21:25:38 dir3
  
0 02/09_21:24:57 file11
0 02/09_21:24:58 file12
0 02/09_21:24:59 file13
0 02/09_21:25:28 file111
0 02/09_21:25:29 file1111
  
0 02/09_21:25:06 file22
0 02/09_21:25:06 file21
0 02/09_21:25:07 file23
0 02/09_21:25:32 file222
0 02/09_21:25:33 file2222
  
0 02/09_21:25:10 file31
0 02/09_21:25:11 file32
0 02/09_21:25:12 file33
0 02/09_21:25:37 file333
0 02/09_21:25:38 file3333

$ python main.py 

=== ex1 (mtime) ===
basedir
('basedir', 'file0')
basedir/dir1
('basedir/dir1', 'file11')
('basedir/dir1', 'file12')
('basedir/dir1', 'file13')
('basedir/dir1', 'file111')
('basedir/dir1', 'file1111')
basedir/dir2
('basedir/dir2', 'file21')
('basedir/dir2', 'file22')
('basedir/dir2', 'file23')
('basedir/dir2', 'file222')
('basedir/dir2', 'file2222')
basedir/dir3
('basedir/dir3', 'file31')
('basedir/dir3', 'file32')
('basedir/dir3', 'file33')
('basedir/dir3', 'file333')
('basedir/dir3', 'file3333')

=== ex2 (mtime/reverse) ===
basedir
('basedir', 'file0')
basedir/dir3
('basedir/dir3', 'file3333')
('basedir/dir3', 'file333')
('basedir/dir3', 'file33')
('basedir/dir3', 'file32')
('basedir/dir3', 'file31')
basedir/dir2
('basedir/dir2', 'file2222')
('basedir/dir2', 'file222')
('basedir/dir2', 'file23')
('basedir/dir2', 'file21')
('basedir/dir2', 'file22')
basedir/dir1
('basedir/dir1', 'file1111')
('basedir/dir1', 'file111')
('basedir/dir1', 'file13')
('basedir/dir1', 'file12')
('basedir/dir1', 'file11')

=== ex3 (no_file) ===
basedir
('basedir', 'file0')
basedir/dir1
('basedir/dir1', 'file13')
basedir/dir2
('basedir/dir2', 'file23')
basedir/dir3
('basedir/dir3', 'file33')
('basedir/dir3', 'file333')
('basedir/dir3', 'file3333')

=== ex4 (yes_path) ===
basedir/dir1
('basedir/dir1', 'file11')
('basedir/dir1', 'file111')
('basedir/dir1', 'file1111')
('basedir/dir1', 'file12')
('basedir/dir1', 'file13')

=== ex5 (no_root/len/reverse) ===
basedir
('basedir', 'file0')
basedir/dir1
('basedir/dir1', 'file1111')
('basedir/dir1', 'file111')
('basedir/dir1', 'file11')
('basedir/dir1', 'file12')
('basedir/dir1', 'file13')
basedir/dir3
('basedir/dir3', 'file3333')
('basedir/dir3', 'file333')
('basedir/dir3', 'file31')
('basedir/dir3', 'file32')
('basedir/dir3', 'file33')

=== ex6 (yes_file/yes_root) ===
basedir/dir2
('basedir/dir2', 'file22')
('basedir/dir2', 'file222')
('basedir/dir2', 'file2222')

*1:to walk somewhere in a slow relaxed way

*2:rubyのDir.glob()は**/を指定することで再帰的にディレクトリ階層を辿ることができる

OpenCV入門

Max OS X(10.10.5)でOpenCVに入門してみた。写真はtoeさんです。 導入はbrewで簡単にインストールできるし、インクルード検索パスやライブラリ検索パスもpkg-configで簡単に指定できる。

ネガポジ反転は、~演算子(ビット反転・1の補数)を実行するだけで取り出せる、というところも面白い。Mat型において、~演算子オーバーロードされているのだろう。 最後に、演算子オーバーロードのコード例を追記してみた。

CascadeClassifierによる分類では、loadするデータを変えることによって、どんな特徴を矩形抽出するかを変えている。顔の特徴と下半身の特徴。顔の矩形座標が取れれば、あとはFacebookのようにフレンドリストの教師データから名前を割り当てることはできそう。これはやってみたいところ。

$ brew update
$ brew tap homebrew/science
$ brew install opencv
$ g++ opencv.cpp `pkg-config --cflags opencv` `pkg-config --libs opencv`
$ pkg-config --cflags opencv
-I/usr/local/Cellar/opencv/2.4.12_2/include/opencv -I/usr/local/Cellar/opencv/2.4.12_2/include 

$ pkg-config --libs opencv
-L/usr/local/Cellar/opencv/2.4.12_2/lib -lopencv_calib3d -lopencv_contrib -lopencv_core -lopencv_features2d -lopencv_flann -lopencv_gpu -lopencv_highgui -lopencv_imgproc -lopencv_legacy -lopencv_ml -lopencv_nonfree -lopencv_objdetect -lopencv_ocl -lopencv_photo -lopencv_stitching -lopencv_superres -lopencv_ts -lopencv_video -lopencv_videostab

gist.github.com

f:id:nishidy:20160202001020j:plainf:id:nishidy:20160202001027j:plainf:id:nishidy:20160202001033j:plainf:id:nishidy:20160202001617j:plainf:id:nishidy:20160202001048j:plainf:id:nishidy:20160202001053j:plainf:id:nishidy:20160202003702j:plain

#include <iostream>
using namespace std;

class Klass{
    public:
    int i;
    int j;
    Klass():i(0),j(1){
    }
    Klass* operator ~ () {
        Klass* obj = new Klass();
        obj->i = ~(this->i);
        obj->j = ~(this->j);
        return obj;
    }

};

int main(int argc, char* argv[]){
    Klass* obj = new Klass();
    Klass* rev = ~(*obj);
    printf("%d,%d\n",obj->i,obj->j);
    printf("%d,%d\n",rev->i,rev->j);
    return 0;
}
$ ./a.out 
0,1
-1,-2

Pythonのpickleでserializeできない問題の回避策(dill)

読み込んだデータをシリアライズしておきたい場合、pickleを使えば任意のクラスのオブジェクトを扱える。ファイルを開いて、dumpで書き出し、loadで読み出しを行えば良い。しかし、ある構造を持つクラスのオブジェクトをpickleでシリアライズしようとすると、以下のようにエラーとなる。

$ python --version
Python 3.4.3

$ python pickle_test.py 
Traceback (most recent call last):
  File "pickle_test.py", line 15, in <module>
    pickle.dump(klass,f)
_pickle.PicklingError: Can't pickle <function Klass.__init__.<locals>.<lambda> at 0x107728ae8>: attribute lookup <lambda> on __main__ failed

クラスの内容は以下の通りで、defaultdictをネストしているので、lambdaを与えている。

$ cat pickle_test.py
# coding: utf-8

import pickle
from collections import defaultdict

class Klass:
    def __init__(self):
        self.prop = defaultdict(lambda:defaultdict(int))

klass = Klass()

klass.prop["x"]["y"] += 10

with open("test.pickle","wb") as f:
    pickle.dump(klass,f)

with open("test.pickle","rb") as f:
    newklass = pickle.load(f)

print(newklass.prop["x"]["y"])

以下をみると、同じようなエラーがlambdaを使った際に発生するようだ。

PicklingError: Can't pickle <type 'function'>: attribute lookup __builtin__.function failed - あと5分だけ

そこで、defaultdictの引数にlambdaではなく関数を渡すようにするとうまくいく。一応、defaultdictのネストにも対応できる。もう少し賢いやり方があるかもしれない。

$ cat pickle_test2.py 
# coding: utf-8

import pickle
from collections import defaultdict

class Val:
    def __init__(self):
        self.vprop= defaultdict(int)

def ddv():
    return defaultdict(Val)

def dd2():
    return defaultdict(dd)

def dd():
    return defaultdict(int)

class Klass:
    def __init__(self):
        self.prop = defaultdict(dd)
        self.prop2= defaultdict(dd2)
        self.prop3= defaultdict(ddv)

klass = Klass()

klass.prop["x"]["y"] += 10
klass.prop2["x"]["y"]["z"] += 100
klass.prop3["x"]["y"].vprop["z"] += 1000

with open("test.pickle","wb") as f:
    pickle.dump(klass,f)

with open("test.pickle","rb") as f:
    newklass = pickle.load(f)

print(newklass.prop["x"]["y"])
print(newklass.prop2["x"]["y"]["z"])
print(newklass.prop3["x"]["y"].vprop["z"])

$ python pickle_test2.py 
10
100
1000

追記 dill

コメントで教えていただきましたdillを使えば良いことが分かりました。もうpickle必要無さそうですね。dillはpipでインストールします。

$ cat dill_test.py
# coding: utf-8

import dill
from collections import defaultdict

class Val:
    def __init__(self):
        self.vprop= defaultdict(int)

class Klass:
    def __init__(self):
        self.prop = defaultdict(lambda:defaultdict(int))
        self.prop2= defaultdict(lambda:defaultdict(lambda:defaultdict(int)))
        self.prop3= defaultdict(lambda:defaultdict(Val))

klass = Klass()

klass.prop["x"]["y"] += 10
klass.prop2["x"]["y"]["z"] += 100
klass.prop3["x"]["y"].vprop["z"] += 1000

with open("test.pickle","wb") as f:
    dill.dump(klass,f)

with open("test.pickle","rb") as f:
    newklass = dill.load(f)

print(newklass.prop["x"]["y"])
print(newklass.prop2["x"]["y"]["z"])
print(newklass.prop3["x"]["y"].vprop["z"])
$ pip install dill
...

$ python dill_test.py 
10
100
1000

Pythonで統計処理した結果を表示する場合の便利関数を書いた

  • 全てのキーの組み合わせが無い場合でも、defaultdictの初期値を用いて表を完成させるように修正。
  • pandasのMultiIndexを使って表示する処理を追記。

統計処理ではネストされた辞書(dict)が多用することで、比較的簡単に所望の複合キーに対応する値を取得しておくことができる。例えば、population[prefecture][gender][age]のようなものだ。これを例えば、横軸prefecture、縦軸gender x ageという具合に、三次元のグラフが出来る表として出力するとき、第一次辞書のキー(この場合はprefecture)を横軸に取れば良いのだが、横軸方向に値を表示していくことを考えると、少し面倒な処理が必要になる。このような表は良く使うので、そのための関数を持つライブラリを書いて使ってみた。もし、横軸にageを取りたければ、population[age][prefecture][gender]というネストされた辞書を作って集計しておけば良い。

実質的にネストの次元はおそらく際限なく増やすことが可能だが、今回は5次元までとしている。もう少し機能を追加したら、PYPIしてみるのもいいかも。月や曜日でもうまくソートできるようにしたい。また、第一次辞書のキーだけではなく任意の辞書のキーを横軸に取れるようにする、など。表示形式としてはセパレータを指定できるので、TSVやCSVにも対応できる。

なお、defaultdictは、キーに対するデフォルトの値を指定することができる(実際には値ではなく、新しいオブジェクトを返すファクトリ関数を指定する)。キーの存在を確認する必要なく、そのキーに対するデフォルトの値を仮定して処理が書けるのでキーの存在確認を行う必要がなく、複雑な辞書を扱う際にシンプルに書ける。

gist.github.com

$ cat a.py 
from collections import defaultdict
from chart import chart

adata = defaultdict(lambda:defaultdict(int))

n = 0
for a in ["1","2","3"]:
    for b in ["xxx","yyy","zzz"]:
        n += 1
        adata[a][b] = n

chart(adata)
print()

bdata = defaultdict(lambda:defaultdict(lambda:defaultdict(int)))

n = 0
for a in ["A","B","C","D","E","F","G"]:
    for b in ["xxx","yyy","zzz"]:
        for c in ["hhh","iii","jjj"]:
            n += 1
            bdata[a][b][c] = n
chart(bdata,"\t")
print()


cdata = defaultdict(lambda:defaultdict(lambda:defaultdict(int)))

cdata["A"]["xxx"]["hhh"] = 1
cdata["B"]["yyy"]["iii"] = 2
cdata["C"]["zzz"]["jjj"] = 3
cdata["D"]["zzz"]["iii"] = 4
cdata["E"]["yyy"]["hhh"] = 5
cdata["F"]["xxx"]["iii"] = 6
cdata["G"]["xxx"]["jjj"] = 7

chart(cdata,"\t")
print()
chart(cdata)
$ python a.py 
,1,2,3
xxx,1,4,7,
yyy,2,5,8,
zzz,3,6,9,

        A   B   C   D   E   F   G
xxx hhh 1   10  19  28  37  46  55  
xxx iii 2   11  20  29  38  47  56  
xxx jjj 3   12  21  30  39  48  57  
yyy hhh 4   13  22  31  40  49  58  
yyy iii 5   14  23  32  41  50  59  
yyy jjj 6   15  24  33  42  51  60  
zzz hhh 7   16  25  34  43  52  61  
zzz iii 8   17  26  35  44  53  62  
zzz jjj 9   18  27  36  45  54  63  

        A   B   C   D   E   F   G
xxx hhh 1   0   0   0   0   0   0   
xxx iii 0   0   0   0   0   6   0   
xxx jjj 0   0   0   0   0   0   7   
yyy hhh 0   0   0   0   5   0   0   
yyy iii 0   2   0   0   0   0   0   
yyy jjj 0   0   0   0   0   0   0   
zzz hhh 0   0   0   0   0   0   0   
zzz iii 0   0   0   4   0   0   0   
zzz jjj 0   0   3   0   0   0   0   

,,A,B,C,D,E,F,G
xxx,hhh,1,0,0,0,0,0,0,
xxx,iii,0,0,0,0,0,6,0,
xxx,jjj,0,0,0,0,0,0,7,
yyy,hhh,0,0,0,0,5,0,0,
yyy,iii,0,2,0,0,0,0,0,
yyy,jjj,0,0,0,0,0,0,0,
zzz,hhh,0,0,0,0,0,0,0,
zzz,iii,0,0,0,4,0,0,0,
zzz,jjj,0,0,3,0,0,0,0,

pandasのMultiIndexを利用した場合

データ分析で良く使うpandasのMultiIndexを用いた場合、stack() unstack()を使えば、同様の処理がより柔軟に行えるようだ。ただし、ネストされたdictionaryをそのままDataFrameに入れてMultiIndexとして使うことはできず、以下のようなリフォーム処理が必要になる。つまり、キーをタプルとしてdictionaryを作成する。

reformed_bdata = { (key1,key3,key2) : [val3] for key1, val1 in bdata.items() for key2, val2 in val1.items() for key3, val3 in val2.items() }
bdf = pd.DataFrame(reformed_bdata)
print(bdf.stack().stack())

reformed_cdata = { (key1,key3,key2) : [val3] for key1, val1 in cdata.items() for key2, val2 in val1.items() for key3, val3 in val2.items() }
cdf = pd.DataFrame(reformed_cdata)
print(cdf.stack().stack())
           A   B   C   D   E   F   G
0 xxx hhh  1  10  19  28  37  46  55
      iii  2  11  20  29  38  47  56
      jjj  3  12  21  30  39  48  57
  yyy hhh  4  13  22  31  40  49  58
      iii  5  14  23  32  41  50  59
      jjj  6  15  24  33  42  51  60
  zzz hhh  7  16  25  34  43  52  61
      iii  8  17  26  35  44  53  62
      jjj  9  18  27  36  45  54  63

           A  B  C  D  E  F  G
0 xxx hhh  1  0  0  0  0  0  0
      iii  0  0  0  0  0  6  0
      jjj  0  0  0  0  0  0  7
  yyy hhh  0  0  0  0  5  0  0
      iii  0  2  0  0  0  0  0
      jjj  0  0  0  0  0  0  0
  zzz hhh  0  0  0  0  0  0  0
      iii  0  0  0  4  0  0  0
      jjj  0  0  3  0  0  0  0

setdefault

collectionsモジュールをインポートしなくても、setdefaultを使えば、キーの確認をせずにキーが存在するものとして値を扱える。

$ cat setdefault_test.py
a = {}  
a.setdefault("aaa",0)
a["aaa"] += 1
print(a)
           
b = {}  
b.setdefault("aaa",[])
b["aaa"].append(10)
print(b) 

$ python setdefault_test.py
{'aaa': 1}
{'aaa': [10]}