[解決!Python]バイナリファイルを読み書きするには:pickle編:解決!Python
pickleモジュールを使用して、Pythonのオブジェクトを直列化/復元(pickle化/非pickle化、シリアライズ/デシリアライズ)する方法と、その際の注意点を紹介する。
* 本稿は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
Copyright© Digital Advantage Corp. All Rights Reserved.