Python 3.8 の概要 (その3) - Pickle protocol 5 with out-of-band data
Pythonでは、複雑なデータの交換や保管する場合、よく Pickleモジュール が使われます。Pickleはデータを外部に出力可能な形式に変換してファイルに変換したり、サーバと通信して送信したりします。
Pythonのconcurrent.futures や multiprocessing を使って並列処理を行う場合も、プロセス間のデータ交換に Pickle が使われています。
PEP-574 Pickle protocol 5 with out-of-band data
Pickleは汎用的なデータフォーマットを定義していて、データを作成したハードウェアと異なるアーキテクチャのハード上で読み込んでも、ただしく元のデータを再現できるようになっています。
しかし、現在ではPickleの使い方は多様化しており、そういった汎用的なデータフォーマットだけでは効率的にデータの転送や保管を行えないことが多くなりました。特に、データサイエンス分野で利用する膨大なデータを高速に扱うには、用途に応じてデータを適切に処理する、専用のメカニズムが必要となります。
そこで、Pickleにあらたに5番目のプロトコルとして、Numpyのarrayオブジェクトなどのメモリ上のデータを直接取得して、効率的に出力する形式が開発されました。
Numpy配列のPickle
例として、1億個の乱数からなるNumpyの配列をPickleで保存してみましょう。
data = np.random.rand(100_000_000) pickle.dump(data, open("data.pickle", "wb"))
この場合、手元のマシン(Macbook air 2018-1.6 GHz Intel Core i5) では、pickle.dumps()
に 1.5秒かかります。
新しいPickleフォーマットを利用する場合は、次のようになります。
pickle.dump(data, open("data5.pickle", "wb"), protocol=5)
新しい形式を利用する場合は、protocol=5
と指定します。この場合、pickle.dump()
の処理時間は0.7秒ほどで、通常のPickleの2倍以上高速になっています。
そのかわり、保存したPickeを読み込んで、オブジェクトを復元できるとは保証されていません。たとえば、このファイルを作成したPCと異なるアーキテクチャのハードウェアでロードしても、正しい値として復元できません。プロトコル5を利用する場合は、保存したPickleをロードできるかどうかは、アプリケーションの開発者が責任を持つ必要があります。
プロトコル5をサポートするオブジェクトをPickleする場合、バッファプロトコル を使ってデータを取得し、メモリ上のデータを値の変換せずに直接ファイルに出力できます。このとき、データのコピーが発生しないように考慮されており、大きなデータでも効率よく出力できます。
この例で使用している Numpyの ndarray
オブジェクトは、メモリ上のデータを memoryviewで取得して出力しています。データに __reduce_ex__()
メソッドを実装すると、この動作をカスタマイズできます。
Out-of-band data
プロトコル5では、実際のデータをPickle内に含めず、独自の形式で管理することもできます。データ本体はそれぞれのアプリケーションが用意する、最適な方式で処理します。
buffers = [] pickle.dump(data, open("data5.pickle", "wb"), protocol=5, buffer_callback=buffers.append) with open("data-body.raw", "wb") as f: f.write(buffers[0])
この形式では、各オブジェクトのバイナリデータは buffer_callback
に指定した関数に渡されます。この例では、buffers.append()
を呼び出し、buffers
リストに保管します。このデータはPickle本体には含まれていませんので、独自に別のファイルに保存しておきます。
buffers.append()
には ndarrayのデータが渡されますが、このデータはオブジェクトから取得したバイナリデータそのもので、ここでもデータのコピーは発生していません。もとのndarrayがどれだけ大きくても、メモリの消費量を増やさずにpickleを出力できます。
保存したデータは、次のように復元します。
with open("data-body.raw", "rb") as f: buffers = [f.read()] pickle5.load(open("data5.pickle", "rb"), buffers=buffers)
データの本体は事前にロードし、リストに格納しています。pickle.load()
はこのリストから元データを取得し、オブジェクトを復元します。
サポート状況
Numpyでは、このプロトコルがサポートされる予定です。
Python3.6/3.7でもこの機能を利用するための pickle5 パッケージがリリースされており、Python3.8以外でも利用できるようになっています。