検索
連載

[解決!Python]バイナリファイルを読み書きするには:pickle編解決!Python

pickleモジュールを使用して、Pythonのオブジェクトを直列化/復元(pickle化/非pickle化、シリアライズ/デシリアライズ)する方法と、その際の注意点を紹介する。

PC用表示 関連情報
Share
Tweet
LINE
Hatena
「解決!Python」のインデックス

連載目次

* 本稿は2021年6月1日に公開された記事をPython 3.12.2で動作確認したものです(確認日:2024年3月1日)。


import pickle

favs = ['beer', 'sake']
mydata = {'name': 'かわさき', 'age': 999, 'weight': 123.4, 'favs': favs}

# pickle化してファイルに書き込み
with open('pickled.pkl', 'wb') as f:
    pickle.dump(mydata, f)

# 非pickle化
with open('pickled.pkl', 'rb') as f:
    mydata2 = pickle.load(f)
    favs2 = mydata['favs']

print(mydata2)
# 出力結果
# {'name': 'かわさき', 'age': 999, 'weight': 123.4, 'favs': ['beer', 'sake']}

print(f'mydata2 == mydata: {mydata2 == mydata}'# mydata2 == mydata: True
print(f'mydata2 is mydata: {mydata2 is mydata}'# mydata2 is mydata: False

# クラスのインスタンスのpickle化
class Foo:
    def __init__(self, name, age):
        self.name = name
        self.age = age

foo = Foo('かわさき', 999)

with open('pickled.pkl', 'wb') as f:
    pickle.dump(foo, f)

# クラスのインスタンスの非pickle化
del foo  # インスタンスを削除
with open('pickled.pkl', 'rb') as f:
    foo = pickle.load(f)  # 復元

print(f'name: {foo.name}, age: {foo.age}') # name: かわさき, age: 999

# 関数オブジェクトとクラスオブジェクトのpickle化
def hello():
    print('hello')

with open('pickled.pkl', 'wb') as f:
    pickle.dump(Foo, f)  # 一つのファイルに複数のオブジェクトをpickle化できる
    pickle.dump(hello, f)

with open('pickled.pkl', 'rb') as f:
    Bar = pickle.load(f)  # FooクラスをBarクラスに復元
    greet = pickle.load(f)  # hello関数をgreet関数に復元

bar = Bar('bar', 101)
print(f'name: {bar.name}, age: {bar.age}'# name: bar, age: 101
greet()  # hello

# Fooインスタンスの復元にはFooクラスが定義されている必要がある
foo = Foo('かわさき', 999)
with open('pickled.pkl', 'wb') as f:
    pickle.dump(foo, f)

del Foo, foo  # Fooクラスとそのインスタンスであるfooを削除
with open('pickled.pkl', 'rb') as f:
    foo = pickle.load(f)  # FooクラスがないのでAttributeError例外

class Foo# 上とは別のFooクラスを定義してみる
    def __init__(self, a, b):
        self.a = a
        self.b = b

with open('pickled.pkl', 'rb') as f:
    foo = pickle.load(f)  # 復元できてしまう

print(foo.a)  # AttributeError例外(復元したfooにはa属性はない)


pickleモジュールとは

 Pythonが標準で提供している「pickleモジュール」は、オブジェクトの直列化(シリアライズ)とその復元(デシリアライズ)を行うために使用できる。ここでいう直列化とはPythonのオブジェクトをバイト列に変換する処理のことで、復元とはバイト列をPythonのオブジェクトに変換する処理のことである。直列化によりバイト列に変換されたデータはバイナリファイルに保存したり、バイト列として他のプログラムにネットワークを介して送信したりできる。なお、pickleモジュールでオブジェクトを直列化することをpickle化、復元することを非pickle化と呼ぶ。

 pickleモジュールを使って、pickle化を行うにはそのモジュールが提供するdump関数もしくはdumps関数を呼び出す。前者はpickle化されたオブジェクトがバイナリファイルへ書き込まれ、後者はpickle化された結果(バイト列)が戻り値となる。非pickle化にはload関数もしくはloads関数を呼び出す。前者はバイナリファイルからpickle化されたデータを読み込んで非pickle化するもので、後者はバイト列を受け取ってそれを非pickle化するものだ。

 以下に基本的な構文を示す。

# pickle化
dump(obj, file, protocol=None)
dumps(obj, protocol=None)

# 非pickle化
load(file)
loads(data)


 dump/dumps関数の第1引数にはpickle化するオブジェクトを指定する。dump関数では第2引数にpickle化した結果のバイト列を書き込むバイナリファイル(を表すファイルオブジェクト)を指定する。dump関数の第3引数とdumps関数の第2引数には、pickle化の際に使用するプロトコルのバージョンを指定する。省略時には、pickleモジュールのDEFAULT_PROTOCOL値が指定されたものと見なされる。

 2021年5月現在、pickle化/非pickle化に使われるプロトコルにはバージョン0〜5の6種類があり、Python 3.0〜3.7ではDEFAULT_PROTOCOLの値は3、Python 3.8以降では4となっている。プロトコルバージョン5は大きなサイズのデータを、余計なメモリコピーを行うことなく高速にpickle化/非pickle化を実行するために使われるものだ(本稿では扱わない)。

 load関数の第1引数には非pickle化するデータを格納しているバイナリファイル(を表すファイルオブジェクト)を、loads関数の第1引数には非pickle化するデータ(バイト列)を渡す。pickle化の時点で、使用しているプロトコルのバージョンが、pickle化されるデータストリームの先頭に書き込まれるため、load/loads関数ではこれを指定する必要はない。

 Pythonのオブジェクトをpickle化してバイナリファイルに書き込むコードの例を以下に示す。

import pickle

favs = ['beer', 'sake']
mydata = {'name': 'かわさき', 'age': 999, 'weight': 123.4, 'favs': favs}

# pickle化してファイルに書き込み
with open('pickled.pkl', 'wb') as f:
    pickle.dump(mydata, f)


 ここでは文字列、整数値、浮動小数点数値、リスト、辞書をpickle化している(これらのオブジェクトがpickle化可能であることを意味している)。dumps関数を使って、バイト列に変換するなら次のようになる。

b = pickle.dumps(mydata)
print(b)  # b'\x80\x04\x95O\x00……\x04beer\x94\x8c\x04sake\x94eu.'


 上のコードを実行してバイナリファイルに書き込まれたデータからPythonのオブジェクトを復元するコードの例は次のようになる。

with open('pickled.pkl', 'rb') as f:
    mydata2 = pickle.load(f)
    favs2 = mydata['favs']

print(mydata2)
# 出力結果
# {'name': 'かわさき', 'age': 999, 'weight': 123.4, 'favs': ['beer', 'sake']}


 バイト列へpickle化したものを復元するには次のようになる。

mydata3 = pickle.loads(b)
print(mydata3)  # 上と同じ出力結果


 以下を実行すると、復元されたオブジェクトは元のオブジェクトと同じ値を持つが、異なるオブジェクトであることが分かる。

print(f'mydata2 == mydata: {mydata2 == mydata}'# mydata2 == mydata: True
print(f'mydata2 is mydata: {mydata2 is mydata}'# mydata2 is mydata: False


pickle化できるもの

 Pythonのドキュメント「pickle 化、非 pickle 化できるもの」にはpickle化/非pickle化できるものとして以下が挙げられている。

  • None値、ブーリアン値(True/False)
  • 整数値、浮動小数点数値、複素数値
  • 文字列、バイト列(bytesオブジェクト)、バイト配列(bytearrayオブジェクト)
  • pickle化可能なオブジェクトだけを要素とするリスト、タプル、辞書、集合
  • モジュールトップレベルで定義された組み込み関数、関数(ラムダ式を除く)、クラス
  • __dict__属性の値がpickle化可能なクラスのインスタンス。または__getstate__メソッドの戻り値がpickle化可能なクラスのインスタンス

 これら以外のオブジェクト(例えば、ファイルオブジェクトなど)はpickle化できない。

 def文で定義した関数(関数オブジェクト)やclass文で定義したクラス自身(クラスオブジェクト)もpickle化可能だ。ただし、関数やクラスのコードそのものがpickle化されるのではなく、完全修飾された名前参照(それが定義されているモジュール名と関数名またはクラス名だけ)がpickle化される点には注意すること。簡単にいうと、関数やクラスをpickle化した場合、それを非pickle化する環境にはその関数やクラスを定義しているモジュール(から対応する関数またはクラス)がインポートされている必要があるということだ。

 例として、クラスを定義して、そのインスタンスをpickle化してみよう(関数やクラスのpickle化はその後で見る)。

class Foo:
    def __init__(self, name, age):
        self.name = name
        self.age = age

foo = Foo('かわさき', 999)

with open('pickled.pkl', 'wb') as f:
    pickle.dump(foo, f)

del foo
with open('pickled.pkl', 'rb') as f:
    foo = pickle.load(f)  # 復元

print(f'name: {foo.name}, age: {foo.age}') # name: かわさき, age: 999


 このコードでは、Fooクラスを定義して、そのインスタンスfooを生成した後にpickle化している。その後、もともとのインスタンスを削除してから、バイナリファイルからデータを読み込んで非pickle化している(ここではFooクラスが定義されているので、問題なく非pickle化できている)。

 次に、上で定義したFooクラスに加えて、hello関数を定義して、今度はクラスと関数をpickle化してみよう。実際のコードは次の通り。

def hello():
    print('hello')

with open('pickled.pkl', 'wb') as f:
    pickle.dump(Foo, f)  # 一つのファイルに複数のオブジェクトをpickle化できる
    pickle.dump(hello, f)

with open('pickled.pkl', 'rb') as f:
    Bar = pickle.load(f)  # FooクラスをBarクラスに復元
    greet = pickle.load(f)  # hello関数をgreet関数に復元

bar = Bar('bar', 101)
print(f'name: {bar.name}, age: {bar.age}'# name: bar, age: 101
greet()  # hello


 ここではwith文のブロック内でdump関数を二度呼び出して、1つのファイルにpickle化された複数のデータを書き込んでいる。このとき、FooクラスはBarクラスに、hello関数はgreet関数に復元した。Barクラスのインスタンス生成、greet関数が成功して、Fooクラスとhello関数と同じように振る舞っている点に注目されたい。

 pickle化では完全修飾の名前参照が用いられるので、__main__.Fooという完全修飾の名前参照を用いて復元されたBarクラスはFooクラスと同一のオブジェクトとなる。

print(f'Bar is Foo: {Bar is Foo}'# True


注意点

 ここで、Fooクラスを削除してから、インスタンスfooを非pickle化してみよう(他の環境でpickled.pklファイルを非pickle化することのシミュレートともいえる)。

foo = Foo('かわさき', 999)
with open('pickled.pkl', 'wb') as f:
    pickle.dump(foo, f)

del Foo, foo  # Fooクラスとそのインスタンスであるfooを削除
with open('pickled.pkl', 'rb') as f:
    foo = pickle.load(f)  # FooクラスがないのでAttributeError例外


 この場合、現在のモジュール(ここでは__main__モジュール)のトップレベルではFooクラス(__main__.Fooクラス)が定義されていないので、AttributeError例外となる。

 ここで上とは異なるFooクラスを定義して、非pickle化したらどうなるかを実験してみよう。

class Foo# 上とは別のFooクラスを定義してみる
    def __init__(self, a, b):
        self.a = a
        self.b = b

with open('pickled.pkl', 'rb') as f:
    foo = pickle.load(f)  # 復元できてしまう

print(foo.a)  # AttributeError例外(復元したfooにはa属性はない)


 驚いたことに復元できてしまう(__main__.Fooというクラスが存在していれば、復元が可能だからだ)。つまり、pickle化されたデータを扱う場合、全体的な整合性を取るのはプログラマーに任されるということだ。これ以外にも、pickle化されたデータを改ざんして、任意のコードを実行させるようにすることも可能だ。

 こうしたことから、pickleモジュールは安全ではないことには注意すること。自分が知らないところでpickle化されたファイルを安易に非pickle化しないようにして、非pickle化するときにはpickle化したときと同じ環境を整えるようにしよう。

 最後にラムダ式はpickle化できないことを確認するコードを示しておく。

fnc = lambda x: print(x)
fnc('hello'# hello

pickle.dumps(fnc)  # PicklingError


「解決!Python」のインデックス

解決!Python

Copyright© Digital Advantage Corp. All Rights Reserved.

[an error occurred while processing this directive]
ページトップに戻る