[[pickle]]

pickle

概要

シリアライズをするためのモジュール。標準モジュールに入っている。

pickleを複数形にするとピクルス(pickles)となる。つまり、処理が終わったら消えてしまうメモリ上のオブジェクトを、漬け物のごとく保存が利くようにし、後から使ったり、ネットワーク越しにやりとりできるようにする。

使用例

処理に時間のかかる中間データを後から分析に使いたい場合など、とりあえず保存しておく、というのに便利。

どこかに保存するのに、別に必ずしもPickleでなくても、CSVなどに変換して保存することもできる。だが、手軽さと容量の面から、Pickleの方が楽。

  • CSV
    • ×ファイル出力・読み込み処理を書くのが面倒
    • ×intをいちいちテキストに変換するので容量を取るし遅い
    • ○テキストエディタで開けば中身が確認できるのは利点
  • バイナリ
    • ×ファイル出力・読み込み処理書くのがとても面倒
    • ○容量は、上手くやれば最小限で済む
  • pickle

使い方

保存

import pickle

a = [1, 2, 3]  # 保存対象

# バイナリ書き込みでファイルを開く
with open('a.pickle', mode='wb') as wh:
    pickle.dump(a, wh)

読み込み

import pickle

# バイナリ読み込みでファイルを開く
with open('a.pickle', mode='rb') as rh:
    a = pickle.load(rh)

print(a)
# => [1, 2, 3]

より詳しい資料

注意

保存される範囲

自作クラスでも問題なくpickle化できるが、それは「名前」だけであり、「クラスそのもの」は保存されない。

例えば、以下のようにしてTestAのインスタンスを保存し、

class TestA:
    num = 1
    def func1(self):
        print('Hello!')

# 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クラスが無いと、エラーになる。

# 全く別の環境に、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すると、変更後の処理になる。これも、「処理」でなく「名前」だけを保存していることの現れである。

class TestA:
    num = 1
    # printする内容を変更
    def func1(self):
        # print('Hello!')
        print('Good Bye!')

import pickle

# a.pickleはTestA変更前に保存しておいたもの
with open('a.pickle', mode='rb') as rh:
    a = pickle.load(rh)

# 変更前に保存したものでも、変更後の処理で実行される
a.func1()
# => Good Bye!

また、クラス内のみで宣言された変数は保持されない。これはついうっかり保存されると思いがちなので注意。

# num属性を持つクラス TestA
class TestA:
    num = 1

TestA のインスタンスを保存(コード略)

# (略: a.pickleの読み込み)

print(a.num)
# => AttributeError: 'TestA' object has no attribute 'num'

a.numを読み取ろうとするとAttributeErrorが発生し、そんな属性は記録されていないことがわかる。

インスタンスに対して宣言された変数は保持される。

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)

# (略: a.pickleの読み込み)

print(a.num)
# => 100

pickleできないオブジェクト

一部、pickle化できない種類のオブジェクトがある。

12.1. pickle — Python オブジェクトの直列化 — Python 3.6.3 ドキュメント

よく引っかかる例はlambda関数や、openで開いたファイルハンドラで、これが保存するオブジェクトのどこか1箇所にでも使われていると、pickle.dump()がエラーを出す。(関数内のローカル変数で使われている場合は関係ない。あくまでpickle化するオブジェクトを構成する一部で使われていればの話)

その場合、自作クラスであれば、pickle.dump時に呼ばれる__getstate__()内でpickle化できないオブジェクトの除去を行い、pickle.load時に呼ばれる__setstate__()内で復元作業を行うことで、対応が可能である。

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__()で現在のオブジェクトに適切に変換できる。

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.がわかる特定の形式で保存するという自分ルールを決めるとか。

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

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

本WebサイトはcookieをPHPのセッション識別および左欄目次の開閉状況記憶のために使用しています。同意できる方のみご覧ください。More information about cookies
programming/python/packages/pickle.txt · 最終更新: 2018/03/30 by ikatakos
CC Attribution 4.0 International
Driven by DokuWiki Recent changes RSS feed Valid CSS Valid XHTML 1.0