Edited at

Pythonのシリアライズモジュール pickle marshal dill cloudpickle を比較する

More than 1 year has passed since last update.

こんにちは、CET というチームに所属している @kojisuganuma です。

普段は機械学習エンジニアリングと Splatoon2 をメインでやってます。

Splatoon2 ではパブロをよく使っています(むしろパブロしか使えない)。

よろしくお願いします。

本記事では、python のシリアライズモジュールである pickle, marshal, dill, cloudpickle の比較を行いたいと思います。


対象読者


  • python オブジェクトをシリアライズして、ファイル出力・ストレージアップロードをしたい方

  • pickle, marshal, dill, cloudpickle のどれを使えば良いのか迷っている方

  • 機械学習の学習済みモデルをシリアライズしたい方


結論

cloudpickle が上位互換です。

pickle と同じ使用方法(dump, dumps, load, loads)で使えるので、オブジェクトをシリアライズ・デシリアライズしたいという場合は cloudpickle を使うと良いでしょう。


本題


python オブジェクトをシリアライズするってどういうこと?

そもそもなんですが、python オブジェクトをシリアライズ (serialize, 直列化) するというのは「python オブジェクト階層をバイトストリームに変換する処理」ということを意味しています。逆に、デシリアライズ (deserialize, 非直列化) するというのは「バイトストリームをオブジェクト階層に復元する処理」ということを意味しています。

python は「全てがオブジェクトである」オブジェクト指向な言語であり、生成したオブジェクトは全て階層構造を持っています。その階層構造をバイトストリームと呼ばれるファイル書き出し・読み込み可能な形式に変換、もしくはその逆変換を行う処理がシリアライズ・デシリアライズということです。ファイル書き出しの際はバイナリモードを指定する必要があることを覚えておきましょう。

python オブジェクト階層についてはこちらの記事がわかりやすいので、是非ご覧ください。

Pythonのオブジェクトとクラスのビジュアルガイド


そんなことして何がうれしいの?

ファイル書き出し・読み込み可能になるということは、python オブジェクトがシステムを跨いだポータブルなものとして扱うことができるということです。

特に機械学習の分野では、学習モデルであるオブジェクト(clf.fit(X, y)clf オブジェクトのこと)を学習フェーズと予測フェーズと呼ばれる二つの段階に分けて処理する機構となっており、とあるシステム上で学習したモデルオブジェクトを別のシステム上の予測に使いたいというようなケースがあるため、オブジェクトのシリアライズ・デシリアライズは有用な技術です。ただし別システムとは言っても、シリアライズ化されたオブジェクトを扱えるのは python のみなので python 製のシステムである必要はあります。


比較する

本記事で載せたソースコード一式は github で公開しています。


pickle

pickle は python に標準的に組み込まれているモジュールです。なので pip などでインストールする必要はありません。

pickle では python の全てのオブジェクトをシリアライズできる訳ではなく、シリアライズ可能なオブジェクトは決まっています。python のドキュメントを見ると以下のようになっています。



  • None 、 True 、および False

  • 整数、浮動小数点数、複素数

  • 文字列、バイト列、バイト配列

  • pickle 化可能なオブジェクトからなるタプル、リスト、集合および辞書

  • モジュールのトップレベルで定義された関数 (def で定義されたもののみで lambda で定義されたものは含まない)

  • モジュールのトップレベルで定義されている組込み関数

  • モジュールのトップレベルで定義されているクラス


  • dict 属性を持つクラス、あるいは getstate() メソッドの返り値が pickle 化可能なクラス (詳細は クラスインスタンスの pickle 化 を参照)。


それでは実装例を示しながら pickle でできること・できないことを見ていきます。

import pickle

def dumps_loads(obj):
"""dumps -> file.write -> file.read -> loads"""
with open('/tmp/serialize_test.pkl', 'wb') as f:
f.write(pickle.dumps(obj))

with open('/tmp/serialize_test.pkl', 'rb') as f:
ojb_ = pickle.loads(f.read())

return ojb_

def dumps(obj):
"""dumps -> file.write"""
with open('/tmp/serialize_test.pkl', 'wb') as f:
f.write(pickle.dumps(obj))

def loads():
"""file.read -> loads"""
with open('/tmp/serialize_test.pkl', 'rb') as f:
ojb_ = pickle.loads(f.read())

return ojb_

#####################
# シリアライズ・デシリアライズできる例
#####################
print(dumps_loads(None))
print(dumps_loads(True))
print(dumps_loads(1))
print(dumps_loads(0.1))
print(dumps_loads(1 + 2j))
print(dumps_loads('hoge'))
print(dumps_loads(b'hoge'))
print(dumps_loads(b'\x02\x1f\xa0'))
print(dumps_loads((None, 0.1, 'hoge')))
print(dumps_loads([None, 0.1, 'hoge']))
print(dumps_loads({None, 0.1, 'hoge'}))
print(dumps_loads({'None': None, '0.1': 0.1, 'hoge': 'hoge'}))

def hoge():
return 'hoge'

print(dumps_loads(hoge))
print(dumps_loads(hoge()))

class Fuga():
def __init__(self):
self.a = 'Fuga'

print(dumps_loads(Fuga))
print(dumps_loads(Fuga()))

#####################
# シリアライズできない例
#####################
class Piyo:
def __init__(self):
f = lambda x: x**2 # ローカル変数に無名関数を使うはセーフ
self.f = lambda x: x**2 # データ属性に無名関数 lambda を使うとエラー

print(dumps_loads(Piyo()))

#####################
# デシリアライズできない例
#####################
FOO = 'FOO'

def foo():
return FOO

dumps(foo)
print(loads()()) # グローバル変数 FOO が参照できる

# 普段 del をすることはあまり無いが、シリアライズとデシリアライズを別環境で動作させた場合などを想定。
# シリアライズする側でグローバル変数を定義 & 関数やクラスなどがそれを参照している場合、デシリアライズする側でも定義する必要がある。
del FOO
print(loads()()) # グローバル変数 FOO が参照できないのでエラー


できないこと

他にもあると思いますが、よく当たる事象を挙げます。


  • データ属性に無名関数 lambda を使ったクラスインスタンスのシリアライズ

  • シリアライズする側でグローバル変数を定義 & その変数を参照しているオブジェクトをシリアライズした場合で、その変数が名前空間内に存在しない環境でのデシリアライズ


marshal

pickle 同様、marshal も python に標準的に組み込まれているモジュールです。

しかし、


一般的に Python オブジェクトを直列化する方法としては pickle を選ぶべきです。


公式が言っているので無視しましょう。


dill

dill は python の 3rd party モジュールです。以下のように pip コマンドでインストールすることができます。

https://pypi.org/project/dill/

$ pip install dill

それでは実装例を示しながら dill でできるようになったこと・できないことを見ていきます。

import dill

def dumps_loads(obj):
"""dumps -> file.write -> file.read -> loads"""
with open('/tmp/serialize_test.pkl', 'wb') as f:
f.write(dill.dumps(obj))

with open('/tmp/serialize_test.pkl', 'rb') as f:
ojb_ = dill.loads(f.read())

return ojb_

def dumps(obj):
"""dumps -> file.write"""
with open('/tmp/serialize_test.pkl', 'wb') as f:
f.write(dill.dumps(obj))

def loads():
"""file.read -> loads"""
with open('/tmp/serialize_test.pkl', 'rb') as f:
ojb_ = dill.loads(f.read())

return ojb_

#####################
# シリアライズ・デシリアライズできる例 (pickleでできたことはできる)
#####################
print(dumps_loads(None))
print(dumps_loads(True))
print(dumps_loads(1))
print(dumps_loads(0.1))
print(dumps_loads(1 + 2j))
print(dumps_loads('hoge'))
print(dumps_loads(b'hoge'))
print(dumps_loads(b'\x02\x1f\xa0'))
print(dumps_loads((None, 0.1, 'hoge')))
print(dumps_loads([None, 0.1, 'hoge']))
print(dumps_loads({None, 0.1, 'hoge'}))
print(dumps_loads({'None': None, '0.1': 0.1, 'hoge': 'hoge'}))

def hoge():
return 'hoge'

print(dumps_loads(hoge))
print(dumps_loads(hoge()))

class Fuga():
def __init__(self):
self.a = 'Fuga'

print(dumps_loads(Fuga))
print(dumps_loads(Fuga()))

#####################
# シリアライズできるようになった例
#####################
class Piyo:
def __init__(self):
f = lambda x: x**2 # ローカル変数に無名関数を使うはセーフ
self.f = lambda x: x**2 # データ属性に無名関数 lambda を使うのもセーフ

print(dumps_loads(Piyo()))

#####################
# デシリアライズできない例
#####################
FOO = 'FOO'

def foo():
return FOO

dumps(foo)
print(loads()()) # グローバル変数 FOO が参照できる

# 普段 del をすることはあまり無いが、シリアライズとデシリアライズを別環境で動作させた場合などを想定。
# シリアライズする側でグローバル変数を定義 & 関数やクラスなどがそれを参照している場合、デシリアライズする側でも定義する必要がある。
del FOO
print(loads()()) # グローバル変数 FOO が参照できないのでエラー


できるようになったこと

他にもあると思いますが、よく当たる事象を挙げます。


  • データ属性に無名関数 lambda を使ったクラスインスタンスのシリアライズ


できないこと

他にもあると思いますが、よく当たる事象を挙げます。


  • シリアライズする側でグローバル変数を定義 & その変数を参照しているオブジェクトをシリアライズした場合で、その変数が名前空間内に存在しない環境でのデシリアライズ


cloudpickle

cloudpickle は python の 3rd party モジュールです。以下のように pip コマンドでインストールすることができます。

https://github.com/cloudpipe/cloudpickle

$ pip install cloudpickle

それでは実装例を示しながら cloudpickle でできるようになったことを見ていきます。

import cloudpickle

def dumps_loads(obj):
"""dumps -> file.write -> file.read -> loads"""
with open('/tmp/serialize_test.pkl', 'wb') as f:
f.write(cloudpickle.dumps(obj))

with open('/tmp/serialize_test.pkl', 'rb') as f:
ojb_ = cloudpickle.loads(f.read())

return ojb_

def dumps(obj):
"""dumps -> file.write"""
with open('/tmp/serialize_test.pkl', 'wb') as f:
f.write(cloudpickle.dumps(obj))

def loads():
"""file.read -> loads"""
with open('/tmp/serialize_test.pkl', 'rb') as f:
ojb_ = cloudpickle.loads(f.read())

return ojb_

#####################
# シリアライズ・デシリアライズできる例 (pickleでできたことはできる)
#####################
print(dumps_loads(None))
print(dumps_loads(True))
print(dumps_loads(1))
print(dumps_loads(0.1))
print(dumps_loads(1 + 2j))
print(dumps_loads('hoge'))
print(dumps_loads(b'hoge'))
print(dumps_loads(b'\x02\x1f\xa0'))
print(dumps_loads((None, 0.1, 'hoge')))
print(dumps_loads([None, 0.1, 'hoge']))
print(dumps_loads({None, 0.1, 'hoge'}))
print(dumps_loads({'None': None, '0.1': 0.1, 'hoge': 'hoge'}))

def hoge():
return 'hoge'

print(dumps_loads(hoge))
print(dumps_loads(hoge()))

class Fuga():
def __init__(self):
self.a = 'Fuga'

print(dumps_loads(Fuga))
print(dumps_loads(Fuga()))

#####################
# シリアライズできるようになった例
#####################
class Piyo:
def __init__(self):
f = lambda x: x**2 # ローカル変数に無名関数を使うはセーフ
self.f = lambda x: x**2 # データ属性に無名関数 lambda を使うのもセーフ

print(dumps_loads(Piyo()))

#####################
# デシリアライズできるようになった例
#####################
FOO = 'FOO'

def foo():
return FOO

dumps(foo)
print(loads()()) # グローバル変数 FOO が参照できる。

# 普段 del をすることはあまり無いが、シリアライズとデシリアライズを別環境で動作させた場合などを想定。
del FOO
print(loads()()) # 変数 FOO が参照できる。なお変数 FOO はグローバル変数ではないことに注意。


できるようになったこと

他にもあると思いますが、よく当たる事象を挙げます。


  • データ属性に無名関数 lambda を使ったクラスインスタンスのシリアライズ

  • シリアライズする側でグローバル変数を定義 & その変数を参照しているオブジェクトをシリアライズした場合で、その変数が名前空間内に存在しない環境でのデシリアライズ


終わりに

最初は pickle を使って無名関数の罠にハマり、次に dill を使ってグローバル変数の罠にハマり、cloudpickle でようやく落ち着くことができました。cloudpickle を使っていれば機械学習の学習済みモデルのシリアライズ・デシリアライズで困らなくなりました。

でもまた新しい罠が待ち構えているんだろうなぁ...という妄想をしながら日々を送っています。自分が気づいていない cloudpickle の罠にすでにハマっている猛者の方、是非コメントしてください!

< 指摘・要望・マサカリ待ってます!


ぼやき

薄っぺらい内容なのに長々と書いてしまってすみませんでした...