概要
pickleはPython独自のデータシリアライズフォーマットで非常に強力な仕組みだが、その背後の動作は高い柔軟性と過去の経緯より単純ではない。
ここでは、主要なbuiltinクラス以外(マイナーなbuiltinクラス、標準・非標準のライブラリ・ユーザ定義クラスなど)がpickle化, unpickle化される過程をまとめ、どのようにすれば効率的にpickle化できるユーザ定義クラスが作れるかまとめた。
なお、ここでは Python 3.3.5 及び Pickle Protocol Version 3 を前提にして話を進める。
Python 3.4 より Protocol Version 4 が導入されたが、内部処理はさらに複雑になったため、まずPython 3.3のコードで理解をするは効率的かと思う。
pickle化の処理の流れ
主に、下記のメソッドを追っていけば理解できる。
class _Pickler:
def save(self, obj, save_persistent_id=True):
...
def save_reduce(self, func, args, state=None,
listitems=None, dictitems=None, obj=None):
...
def save_global(self, obj, name=None, pack=struct.pack):
...
static PyObject *
reduce_2(PyObject *obj)
{
...
}
static PyObject *
object_reduce_ex(PyObject *self, PyObject *args)
{
...
}
pickle化の1stステップ
pickle.dump, pickle.dumps
などが呼ばれると、全ては下記の処理によりpickle化される。
pickler = pickle.Pickler(fileobj, protocol)
pickler.dump(obj)
なお、Picklerクラスは、
- C実装の
_pickle.Pickler
、または - Python実装の
pickle._Pickler
で、それぞれ下記の場所に実体がある。 - Modules/_pickler.c で定義されている
static PyTypeObject Pickler_Type;
- Lib/pickle.py で定義されている
class _Pickler
通常はC実装が優先的に使用されるが、importに失敗した場合はPython実装が使用される。
ここでは仕組みの理解が主目的なので、Python実装を中心に話を進める。
個別のオブジェクトは、pickler.save(obj)
により再帰的にpickle化されていく。
まず循環参照や複数個所など参照されているなどによる既出のオブジェクトは、この関数の前半で適切に前方への参照としてpickle化される。
主要なbuiltinクラスの場合
下記のbuiltinクラスおよび定数は多く使われるため、Pickleは専用の効率的な処理を実装している。
このため、本稿での説明には該当しないので割愛する。
int, float, str, bytes, list, tuple, dict, bool, None
これ以外のクラスの場合、以下に示す手順でpickle化される。
クラスオブジェクト又は関数の場合
pickle化対象がクラスオブジェクト(すなわちisinstance(obj, type) == True
)の場合又は関数の場合、obj.__module__, obj.__name__
が文字列として記録される。
unpickle化では、必要なモジュールをimportしたうえで、この変数名で参照できる値がunpickle化される。
すなわち、モジュールのグローバルな名前空間上で定義されたクラスや関数以外はpickle化できない。
もちろん関数やクラスのロジックも記憶されない、PythonはLISPではない。
copyregモジュールに登録されたクラスのオブジェクトの場合
次に、copyregモジュールにグローバル定義された辞書より、copyreg.dispatch_table[type(obj)]
の存在がチェックされる。
import copyreg
if type(obj) in copyreg.dispatch_table:
reduce = copyreg.dispatch_table[type(obj)]
rv = reduce(obj)
戻り値rv
の内容については後述。
このように、copyreg.dispatch_table
に登録された関数が最優先でpickle化に使用される。
このため、定義を変更できないクラスであってもpickle化/unpickle化の挙動を変更することができる。極端な話、時刻オブジェクトをpickle化/unpickle化すると正規表現オブジェクトになるように仕込むこともできる。
import pickle
import copyreg
import datetime
import re
def reduce_datetime_to_regexp(x):
return re.compile, (r'[spam]+',)
copyreg.pickle(datetime.datetime, reduce_datetime_to_regexp)
a = datetime.datetime.now()
b = pickle.loads(pickle.dumps(a))
print(a, b) # 2014-10-05 10:24:12.177959 re.compile('[spam]+') のように出力
辞書dispatch_table
への追加は、copyreg.pickle(type, func)
を介して行う。
なお、辞書pickler.dispatch_table
があった場合、copyreg.dispatch_table
の代わりにこちらが使用される。
特定用途のpickle化の時のみ挙動を変えたい場合は、こちらの方が安全である。
import pickle
import copyreg
import datetime
import re
import io
def reduce_datetime_to_regexp(x):
return re.compile, (r'[spam]+',)
a = datetime.datetime.now()
with io.BytesIO() as fp:
pickler = pickle.Pickler(fp)
pickler.dispatch_table = copyreg.dispatch_table.copy()
pickler.dispatch_table[datetime.datetime] = reduce_datetime_to_regexp
pickler.dump(a)
b = pickle.loads(fp.getvalue())
print(a, b) # 2014-10-05 10:24:12.177959 re.compile('[spam]+') のように出力
obj.__reduce_ex__
が定義されている場合
メソッドobj.__reduce_ex__
が定義されている場合、
rv = obj.__reduce_ex__(protocol_version)
が呼び出される。
戻り値rv
の内容については後述。
obj.__reduce__
が定義されている場合
メソッドobj.__reduce__
が定義されている場合、
rv = obj.__reduce__()
が呼び出される。
戻り値rv
の内容については後述。
__reduce__
の必要性
現状では無いと思われる。常に__reduce_ex__
を使えば良い。
こちらの方が先に検索されるので、若干であるが早くもなる。
プロトコル変数も、使わなければ無視すれば良い。
何も特別な定義をしていない場合
pickle化/unpickle化のために、何も特別なメソッドなどを書いていない場合は、最終手段としてobject
標準のreduce
処理が行われる。
これは、言わば「大抵のオブジェクトでこのまま使えそうな、最もユニバーサルな、最大公約数的な__reduce_ex__
の実装」であり、大変参考になるのだが、残念ながらC言語で実装されていて理解は難しい。
この当該箇所を、エラー処理など割愛し、大まかな流れをPythonで実装すると下記のようになる。
class object:
def __reduce_ex__(self, proto):
from copyreg import __newobj__
if hasattr(self, '__getnewargs__'):
args = self.__getnewargs__()
else:
args = ()
if hasattr(self, '__getstate__'):
state = self.__getstate__()
elif hasattr(type(self), '__slots__'):
state = self.__dict__, {k: getattr(self, k) for k in type(self).__slots__}
else:
state = self.__dict__
if isinstance(self, list):
listitems = self
else:
listitems = None
if isinstance(self, dict):
dictitems = self.items()
else:
listitems = None
return __newobj__, (type(self),)+args, state, listitems, dictitems
上を参照してわかる通り、object.__reduce_ex__
に頼るとしても、__getnewargs__, __getstate__
のメソッドを定義すれば、細かい挙動の変更が可能となる。
これらの関数は、自前で__reduce_ex__, __reduce__
を定義した場合は、明示的に呼ばない限り利用されない。
__getnewargs__
pickle化可能なタプルを返すメソッド。
これが定義されていると、unpickle化での__new__
への引数(__init__
ではない)がカスタマイズできる。
先頭の引数(クラスオブジェクト)は含まない。
__getstate__
これが定義されていると、unpickle化での__setstate__
の引数、又は__setstate__
が存在しない場合の__dict__
及びスロットの初期値がカスタマイズできる。
__reduce_ex__, __reduce__
及びcopyreg登録関数が返すべき値
上記の処理で、各関数が返すべき値rv
は、
- 文字列
- 要素数2以上5以下のタプル。要素数が5未満の場合は、不足分は
None
と解釈される。
である。
type(rv) is str
であった場合
type(obj).__module__, rv
が文字列としてpickle化では記録され、unpickle化には適切にモジュールがimportされた上でこの名前で参照されるオブジェクトが返される。
このメカニズムは、シングルトンオブジェクトなどをpickle化する際に有効に利用できる。
type(rv) is tuple
であった場合
タプルの要素(2以上5以下)は下記の通り
-
func
- unpickle時にオブジェクトを生成するpickle可能かつ呼び出し可能オブジェクト(典型的にはクラスオブジェクト)。ただし、func.__name__ == "__newobj__"
の場合は例外で後述。 -
args
- pickle可能な要素からなるタプル。func
の呼び出し時のパラメータとして使用される。 -
state
- オブジェクトの状態をunpickleするためのオブジェクト。省略可能。None
でも良い。 -
listitems
-list
ライクなオブジェクトの要素を返すイテレーション可能オブジェクト。省略可能。None
でも良い。 -
dictitems
-dict
ライクなオブジェクトのキーと要素を返すイテレーション可能オブジェクト。イテレータが返す値は、キーと要素のペアでなければならない。典型的にはdict_object.items()
。省略可能。None
でも良い。
func.__name__ == "__newobj__"
の場合
この場合、args[0]
がクラスオブジェクトとして解釈されargs
を引数としてクラスオブジェクトが作成される。この時は__init__
は呼ばれない。
このような条件のfunc
オブジェクトが必要ならば、copyregモジュールに既に宣言されたものがある。
def __newobj__(cls, *args):
return cls.__new__(cls, *args)
このcopyreg.__newobj__
は、通常の関数と解釈しても同様の動作をするように実装されて入るが、実際には実行されることはない。
state
の値の解釈
以下のように解釈される。
- unpickle化対象のオブジェクトが
obj.__setstate__
を持っていれば、そのメソッドへの引数。 - 要素2のタプルであれば、
state[0]
がobj.__items__
の内容を示す辞書、state[1]
がtype(obj).__slots__
の内容を示す辞書。いずれもNone
でも良い。 - 単一の辞書であれば、
obj.__items__
の内容
unpickle化の処理の流れ
主に、下記のメソッドを追っていけば理解できる。
class _Unpickler:
def load_newobj(self):
...
def load_reduce(self):
...
def load_build(self):
...
def load_global(self):
...
unpickle化の1stステップ
pickle.load, pickle.loads
などが呼ばれると、全ては下記の処理によりunpickle化される。
unpickler = pickle.Unpickler(fileobj)
unpickler.load()
なお、Unpicklerクラスは、
- C実装の
_pickle.Unpickler
、または - Python実装の
pickle._Unpickler
で、それぞれ下記の場所に実体がある。 - Modules/_pickler.c で定義されている
static PyTypeObject Unpickler_Type;
- Lib/pickle.py で定義されている
class _Unpickler
pickleデータ内の要素に応じて、opcodeと呼ばれるIDに応じ、unpickler.load_xxx()
を順次呼び出しながら、オブジェクトを復元していく。
global opcodeデータのunpickle化
クラス、関数、__reduce_ex__
が文字列を返したケースなどでは"modulename.varname"
という文字列がそのまま記録されている。
この場合、必要ならばモジュールをimportし、対応する値を出力する。
unpicklerによる新たなオブジェクトの生成は行われない。
newobj, reduce, build opcodeデータのunpickle化
__reduce_ex__
等により返される5要素のタプルを使用してpickle化された場合、これらの処理によりオブジェクトがunpickle化される。
この処理に対応するload_newobj, load_reduce, load_build
の各メソッドの概要を平易な流れで書き直せば、おおむね下記のようになる。
def unpickle_something():
func, args, state, listitems, dictitems = load_from_pickle_stream()
if getattr(func, '__name__', None) == '__newobj__':
obj = args[0].__new__(*args)
else:
obj = func(*args)
if lisitems is not None:
for x in listitems:
obj.append(x)
if dictitems is not None:
for k, v in dictitems:
obj[k] = v
if hasattr(obj, '__setstate__'):
obj.__setstate__(state)
elif type(state) is tuple and len(state) == 2:
for k, v in state[0].items():
obj.__dict__[k] = v
for k, v in state[1].items():
setattr(obj, k, v)
else:
for k, v in state.items():
obj.__dict__[k] = v
return obj
ケーススタディ
特になにもしなくて良いケース
次のような条件を満たすケースは、特にpickle化、unpickle化の処理を書かなくても、適切に処理ができる。
- 全ての
__dict__
の中身はpickle化可能で、そのまま復元されても問題ない。 - 全ての
__slots__
に対応する属性の値はpickle化可能で、そのまま復元されても問題ない。 - C言語による実装により、Pythonからアクセス不可能な内部データを持っていない。
-
__new__
に引数を解釈する処理を加えていない。 -
__init__
が呼ばれなくても、属性が正しく復元されればオブジェクトとして矛盾はない。 - listやdictのサブクラスの場合、要素は全てpickle化可能で、そのまま復元されて問題ない。
pickleに含めたくない属性(キャッシュなど)やpickle化できない属性を持つオブジェクト
import pickle
class Sphere:
def __init__(self, radius):
self._radius = radius
@property
def volume(self):
if not hasattr(self, '_volume'):
from math import pi
self._volume = 4/3 * pi * self._radius ** 3
return self._volume
def _main():
sp1 = Sphere(3)
print(sp1.volume)
print(sp1.__reduce_ex__(3))
sp2 = pickle.loads(pickle.dumps(sp1))
print(sp2.volume)
if __name__ == '__main__':
_main()
球を表現するShhereオブジェクトは、体積を表すvolumeプロパティにアクセスすると、内部に計算結果をキャッシュする。
これをそのままpickle化すると、キャッシュされた体積まで一緒に保存されるため、データサーズが大きくなる。
これを削除したい。
class Sphere:
def __init__(self, radius):
self._radius = radius
@property
def volume(self):
if not hasattr(self, '_volume'):
from math import pi
self._volume = 4/3 * pi * self._radius ** 3
return self._volume
def __getstate__(self):
return {'_radius': self._radius}
unpickle後の__dict__
の値を返す__getstate__
メソッドを定義することで、キャッシュがpickle化されることを防げる。
class Sphere:
__slots__ = ['_radius', '_volume']
def __init__(self, radius):
self._radius = radius
@property
def volume(self):
if not hasattr(self, '_volume'):
from math import pi
self._volume = 4/3 * pi * self._radius ** 3
return self._volume
def __getstate__(self):
return None, {'_radius': self._radius}
メモリ効率を向上させるため、__slots__
を定義した場合、__dict__
は存在しなくなるため__getstate__
が返す値も替える必要がある。
この場合2要素のタプルとし、後ろの要素を__slots__
の属性を初期化する辞書とする。
前の要素(__dict__
の初期値)はNone
で構わない。
class Sphere:
__slots__ = ['_radius', '_volume']
def __init__(self, radius):
self._radius = radius
@property
def volume(self):
if not hasattr(self, '_volume'):
from math import pi
self._volume = 4/3 * pi * self._radius ** 3
return self._volume
def __getstate__(self):
return self._radius
def __setstate__(self, state):
self._radius = state
pickleすべき値が半径のみと決まっているなら、辞書ではなくself._radius
の値そのものを__getstate__
で返しても良い。
その場合、対になる__setstate__
も定義する。
__new__
に適切な引数を与えないと生成できないオブジェクト
import pickle
class IntLiterals(tuple):
def __new__(cls, n):
a = '0b{n:b} 0o{n:o} {n:d} 0x{n:X}'.format(n=n).split()
return super(cls, IntLiterals).__new__(cls, a)
def __getnewargs__(self):
return int(self[0], 0),
def _main():
a = IntLiterals(10)
print(a) # ('0b1010', '0o12', '10', '0xA')
print(a.__reduce_ex__(3))
b = pickle.loads(pickle.dumps(a))
print(b)
if __name__ == '__main__':
_main()
__init__
を呼ばないと生成できないオブジェクト
import pickle
class ClosureHolder:
def __init__(self, value):
def _get():
return value
self._get = _get
def get(self):
return self._get()
def __reduce_ex__(self, proto):
return type(self), (self.get(),)
def _main():
a = ClosureHolder('spam')
print(a.get())
print(a.__reduce_ex__(3))
b = pickle.loads(pickle.dumps(a))
print(b.get())
if __name__ == '__main__':
_main()
get
が返す値は、__init__
内でクロージャにより記憶されているので、__init__
を呼び出さないとオブジェクトを作成できない。
このようなケースでは、object.__reduce_ex__
を使用することはできないので、自前で__reduce_ex__
を実装する。
シングルトンオブジェクト
class MySingleton(object):
def __new__(cls, *args, **kwds):
assert mysingleton is None, \
'A singleton of MySingleton has already been created.'
return super(cls, MySingleton).__new__(cls, *args, **kwds)
def __reduce_ex__(self, proto):
return 'mysingleton'
mysingleton = None
mysingleton = MySingleton()
def _main():
import pickle
a = pickle.dumps(mysingleton)
b = pickle.loads(a)
print(b)
if __name__ == '__main__':
_main()
MySingleton
クラスは、mysingleton
グローバル変数に唯一のインスタンスのみを持つとする。
これを正しくunpickleするには、__reduce_ex__
が文字列を返す形式を使えば良い。