シリアライズをするためのモジュール。標準モジュールに入っている。
pickleを複数形にするとピクルス(pickles)となる。つまり、処理が終わったら消えてしまうメモリ上のオブジェクトを、漬け物のごとく保存が利くようにし、後から使ったり、ネットワーク越しにやりとりできるようにする。
処理に時間のかかる中間データを後から分析に使いたい場合など、とりあえず保存しておく、というのに便利。
どこかに保存するのに、別に必ずしもPickleでなくても、CSVなどに変換して保存することもできる。だが、手軽さと容量の面から、Pickleの方が楽。
1 2 3 4 5 6 7 |
import pickle a = [ 1 , 2 , 3 ] # 保存対象 # バイナリ書き込みでファイルを開く with open ( 'a.pickle' , mode = 'wb' ) as wh: pickle.dump(a, wh) |
1 2 3 4 5 6 7 8 |
import pickle # バイナリ読み込みでファイルを開く with open ( 'a.pickle' , mode = 'rb' ) as rh: a = pickle.load(rh) print (a) # => [1, 2, 3] |
自作クラスでも問題なくpickle化できるが、それは「名前」だけであり、「クラスそのもの」は保存されない。
例えば、以下のようにしてTestAのインスタンスを保存し、
1 2 3 4 |
class TestA: num = 1 def func1( self ): print ( 'Hello!' ) |
1 2 3 4 5 6 7 |
# TestA のインスタンス a を作りpickleで保存 import pickle from test_a import TestA a = TestA() with open ( 'a.pickle' , mode = 'wb' ) as wh: pickle.dump(a, wh) |
次にこれをpickle.load()
する際は、その環境から同じようにfrom test_a import TestA
によってインポートできるTestAクラスが無いと、エラーになる。
1 2 3 4 5 6 7 |
# 全く別の環境に、a.pickleだけコピーして、TestAクラスはインポートせず実行 import pickle with open ( 'a.pickle' , mode = 'rb' ) as rh: a = pickle.load(rh) # => ModuleNotFoundError: No module named 'test_a' |
また、pickle後に関数の処理を変えてからunpickleすると、変更後の処理になる。これも、「処理」でなく「名前」だけを保存していることの現れである。
1 2 3 4 5 6 |
class TestA: num = 1 # printする内容を変更 def func1( self ): # print('Hello!') print ( 'Good Bye!' ) |
1 2 3 4 5 6 7 8 9 |
import pickle # a.pickleはTestA変更前に保存しておいたもの with open ( 'a.pickle' , mode = 'rb' ) as rh: a = pickle.load(rh) # 変更前に保存したものでも、変更後の処理で実行される a.func1() # => Good Bye! |
また、クラス内のみで宣言された変数は保持されない。これはついうっかり保存されると思いがちなので注意。
1 2 3 |
# num属性を持つクラス TestA class TestA: num = 1 |
TestA のインスタンスを保存(コード略)
1 2 3 4 |
# (略: a.pickleの読み込み) print (a.num) # => AttributeError: 'TestA' object has no attribute 'num' |
a.numを読み取ろうとするとAttributeErrorが発生し、そんな属性は記録されていないことがわかる。
インスタンスに対して宣言された変数は保持される。
1 2 3 4 5 6 7 8 |
import pickle from test_a import TestA a = TestA() a.num + = 99 # このタイミングで、numはインスタンスの属性となる with open ( 'a.pickle' , mode = 'wb' ) as wh: pickle.dump(a, wh) |
1 2 3 4 |
# (略: a.pickleの読み込み) print (a.num) # => 100 |
一部、pickle化できない種類のオブジェクトがある。
12.1. pickle — Python オブジェクトの直列化 — Python 3.6.3 ドキュメント
よく引っかかる例はlambda
関数や、open
で開いたファイルハンドラで、これが保存するオブジェクトのどこか1箇所にでも使われていると、pickle.dump()がエラーを出す。(関数内のローカル変数で使われている場合は関係ない。あくまでpickle化するオブジェクトを構成する一部で使われていればの話)
その場合、自作クラスであれば、pickle.dump
時に呼ばれる__getstate__()
内でpickle化できないオブジェクトの除去を行い、pickle.load
時に呼ばれる__setstate__()
内で復元作業を行うことで、対応が可能である。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
import pickle from collections import defaultdict class TestB: def __init__( self ): # pickle化できないlambdaを持つインスタンス変数 self .dd = defaultdict( lambda : [[],[]]) # pickle時に呼ばれる def __getstate__( self ): state = self .__dict__.copy() state[ 'dd' ] = dict ( self .dd) return state # unpickle時に呼ばれる def __setstate__( self , state): self .__dict__.update(state) # 復元 self .dd = defaultdict( lambda : [[],[]], self .dd) b = TestB() with open (b.pickle, mode = 'wb' ) as wh: pickle.dump(b, wh) |
または、標準モジュールでは無いが「dill
」というモジュールを使うことで、lambdaなども含めた拡張された範囲でのSerializeを行うことができる。表面的な使い方(dump, load)は変わらない。ただ、更新が止まったりするリスクはある。
上記の「Pickle化するクラスは名前のみ保存し、復元する際は現在のモジュールから読み直す」というのは、ドキュメント内にちらっと書かれているが、「バグ修正などが施された最新のものを使った方がよい」という思想からのようだ。
でも実際問題、それは「クラスのAPI(使用方法)が変わらない限り」という前提が付き、それが守られることは、よほど成熟したモジュールでもなければ稀である。
もし自作クラスで、長期的な使用を見据えバージョンによる使用方法の変更に対応するのであれば、pickleにバージョン情報を埋め込んでおけば、__setstate__()で現在のオブジェクトに適切に変換できる。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
import pickle class TestC: def __setstate__( self , state): self .__dict__.update(state) self .ver = = '0.1' : # ver.0.1ではipsum変数に記録されていたのを # ver.0.2からlorem変数に記録するようにしたとする self .lorem = state.ipsum del self .ipsum c = TestC() c.ver = '0.2' # ... |
自作クラスでは無い場合は、ver.がわかる特定の形式で保存するという自分ルールを決めるとか。
1 2 3 4 5 6 |
import pickle data = [ 1 , 2 , 3 ] with open (d.pickle, mode = 'wb' ) as wh: pickle.dump({ver: '0.2' , data: data}, wh) |
個人的に「既に処理済みのpickleがあればそちらから読み込む。無ければ処理後、保存する」という方法は割とよく使う。そのような処理を共通化するデコレータ例。
Python Tips:デコレータに引数を渡したい - Life with Python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
def add_pickle(pickle_path): def _add_pickle(func): def wrapper( * args): if os.path.exists(pickle_path): with open (pickle_path, 'rb' ) as rh: return pickle.load(rh) data = func( * args) with open (pickle_path, 'wb' ) as wh: pickle.dump(data, wh) return data return wrapper return _add_pickle # 使用 @add_pickle ( 'C:\\path\\to\\pickle' ) def heavy_process(): # データ生成処理 return data |