ユーザ定義クラスのpickle, unpickle処理のまとめ

# 概要
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化の処理の流れ
主に、下記のメソッドを追っていけば理解できる。

```py3:Lib/pickle.py
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):
        ...
```

```c:Objects/typeobject.c
static PyObject *
reduce_2(PyObject *obj)
{
    ...
}

static PyObject *
object_reduce_ex(PyObject *self, PyObject *args)
{
    ...
}
```

## pickle化の1stステップ
`pickle.dump, pickle.dumps`などが呼ばれると、全ては下記の処理によりpickle化される。

```py3:sample1.py
pickler = pickle.Pickler(fileobj, protocol)
pickler.dump(obj)
```

なお、Picklerクラスは、
1. C実装の`_pickle.Pickler`、または
2. Python実装の`pickle._Pickler`
で、それぞれ下記の場所に実体がある。
1. Modules/_pickler.c で定義されている `static PyTypeObject Pickler_Type;`
2. 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)]`の存在がチェックされる。

```py3:sample02.py
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化すると正規表現オブジェクトになるように仕込むこともできる。

```py3:sample03.py
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化の時のみ挙動を変えたい場合は、こちらの方が安全である。

```py3:sample03a.py
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__`が定義されている場合、

```py3:sample03.py
rv = obj.__reduce_ex__(protocol_version)
```

が呼び出される。
戻り値`rv`の内容については後述。

## `obj.__reduce__` が定義されている場合
メソッド`obj.__reduce__`が定義されている場合、

```py3:sample03.py
rv = obj.__reduce__()
```

が呼び出される。
戻り値`rv`の内容については後述。

### `__reduce__`の必要性
現状では無いと思われる。常に`__reduce_ex__`を使えば良い。
こちらの方が先に検索されるので、若干であるが早くもなる。
プロトコル変数も、使わなければ無視すれば良い。

## 何も特別な定義をしていない場合
pickle化/unpickle化のために、何も特別なメソッドなどを書いていない場合は、最終手段として`object`標準の`reduce`処理が行われる。
これは、言わば「大抵のオブジェクトでこのまま使えそうな、最もユニバーサルな、最大公約数的な`__reduce_ex__`の実装」であり、大変参考になるのだが、残念ながらC言語で実装されていて理解は難しい。
この当該箇所を、エラー処理など割愛し、大まかな流れをPythonで実装すると下記のようになる。

```py3:object_reduce_ex.py
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以下）は下記の通り

1. `func` - unpickle時にオブジェクトを生成するpickle可能かつ呼び出し可能オブジェクト（典型的にはクラスオブジェクト）。ただし、`func.__name__ == "__newobj__"`の場合は例外で後述。
2. `args` - pickle可能な要素からなるタプル。`func`の呼び出し時のパラメータとして使用される。
3. `state` - オブジェクトの状態をunpickleするためのオブジェクト。省略可能。`None`でも良い。
4. `listitems` - `list`ライクなオブジェクトの要素を返すイテレーション可能オブジェクト。省略可能。`None`でも良い。
5. `dictitems` - `dict`ライクなオブジェクトのキーと要素を返すイテレーション可能オブジェクト。イテレータが返す値は、キーと要素のペアでなければならない。典型的には`dict_object.items()`。省略可能。`None`でも良い。

#### `func.__name__ == "__newobj__"`の場合
この場合、`args[0]`がクラスオブジェクトとして解釈され`args`を引数としてクラスオブジェクトが作成される。この時は`__init__`は呼ばれない。
このような条件の`func`オブジェクトが必要ならば、copyregモジュールに既に宣言されたものがある。

```py3:Lib/copyreg.py
def __newobj__(cls, *args):
    return cls.__new__(cls, *args)
```

この`copyreg.__newobj__`は、通常の関数と解釈しても同様の動作をするように実装されて入るが、実際には実行されることはない。

#### `state`の値の解釈
以下のように解釈される。
1. unpickle化対象のオブジェクトが`obj.__setstate__`を持っていれば、そのメソッドへの引数。
2. 要素2のタプルであれば、`state[0]`が`obj.__items__`の内容を示す辞書、`state[1]`が`type(obj).__slots__`の内容を示す辞書。いずれも`None`でも良い。
3. 単一の辞書であれば、`obj.__items__`の内容

# unpickle化の処理の流れ
主に、下記のメソッドを追っていけば理解できる。

```py3:Lib/pickle.py
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化される。

```py3:sample1.py
unpickler = pickle.Unpickler(fileobj)
unpickler.load()
```

なお、Unpicklerクラスは、
1. C実装の`_pickle.Unpickler`、または
2. Python実装の`pickle._Unpickler`
で、それぞれ下記の場所に実体がある。
1. Modules/_pickler.c で定義されている `static PyTypeObject Unpickler_Type;`
2. 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`の各メソッドの概要を平易な流れで書き直せば、おおむね下記のようになる。

```py3:sample09.py
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化の処理を書かなくても、適切に処理ができる。
1. 全ての`__dict__`の中身はpickle化可能で、そのまま復元されても問題ない。
2. 全ての`__slots__`に対応する属性の値はpickle化可能で、そのまま復元されても問題ない。
3. C言語による実装により、Pythonからアクセス不可能な内部データを持っていない。
4. `__new__`に引数を解釈する処理を加えていない。
5. `__init__`が呼ばれなくても、属性が正しく復元されればオブジェクトとして矛盾はない。
6. listやdictのサブクラスの場合、要素は全てpickle化可能で、そのまま復元されて問題ない。

## pickleに含めたくない属性（キャッシュなど）やpickle化できない属性を持つオブジェクト

```py3:sphere0.py
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化すると、キャッシュされた体積まで一緒に保存されるため、データサーズが大きくなる。
これを削除したい。

```py3:sphere1.py
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化されることを防げる。

```py3:sphere2.py
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`で構わない。

```py3:sphere3.py
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__`に適切な引数を与えないと生成できないオブジェクト

```py3:intliterals.py
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__`を呼ばないと生成できないオブジェクト

```py3:closureholder.py
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__`を実装する。

## シングルトンオブジェクト
```py3:singleton.py
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__`が文字列を返す形式を使えば良い。