Cythonによるパフォーマンス向上
- 作者: Kurt W. Smith,中田秀基,長尾高弘
- 出版社/メーカー: オライリージャパン
- 発売日: 2015/06/19
- メディア: 大型本
- この商品を含むブログ (3件) を見る
Pythonのような書きやすいインタープリタ型言語で書き始めて結果を取得できるところまでいくと、その後にはパフォーマンスを上げるためにC++で書きなおそうか、と思ったりする。かといって最初からC++で書いていては、きちんと動くものを書き切るのに間違いなく時間がかかるし、とにかくまず結果が欲しいときには取るべき有効な手段ではないと思う。
そこでCythonの出番。Pythonのコードはそのままで有効なCythonのコードでもあるので、cythonをインストールして拡張子をpyx
に変更し、import pyximport; pyximport.install()
をつけるだけで速度改善の効果が望める。もちろん、静的な型指定などのチューニングを施すことによって、Pythonのセマンティクスを保ったまま、よりCに近い速度を達成することができる。これだけ簡単に導入できるので、もしCPUバウンドな処理であることが分かっているなら、Cythonを使わない手はないだろう。
どのような場合にどの程度の速度改善が望めるか、以下のようにPythonのコードを実行した結果と、PythonのコードをそのままCythonとしてコンパイルして実行した結果、Cythonとして書き直したコードを実行した結果、を比較した。プログラムの内容は、単にソートを実施しているだけだが、再帰呼び出しの有り・無し、でそれぞれプログラムを変えている。Cythonでは、stdlib.h
のqsort
を使っている。
再帰呼び出し有り、では関数呼び出しのオーバーヘッドが大きくなるため、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などの関数に正規表現を与える場合、rubyやperlなどのように正規表現を直接与えることができず、文字列として与える必要があるため、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引数に与えようと、repl
やeval
を試してみてもうまくいかない。
>>> 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はどこでもマッチする。
$ 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
に与える初期値がそのまま判定結果になってしまうが、それが嫌ならリストを三項演算で判定して初期値を与えれば良い。
$ 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')
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
#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は、キーに対するデフォルトの値を指定することができる(実際には値ではなく、新しいオブジェクトを返すファクトリ関数を指定する)。キーの存在を確認する必要なく、そのキーに対するデフォルトの値を仮定して処理が書けるのでキーの存在確認を行う必要がなく、複雑な辞書を扱う際にシンプルに書ける。
$ 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]}