このBlogは移転しました。今後は aish.dev を御覧ください。

Python 3.8 の概要 (その3) - Pickle protocol 5 with out-of-band data

Pythonでは、複雑なデータの交換や保管する場合、よく Pickleモジュール が使われます。Pickleはデータを外部に出力可能な形式に変換してファイルに変換したり、サーバと通信して送信したりします。

Pythonconcurrent.futuresmultiprocessing を使って並列処理を行う場合も、プロセス間のデータ交換に 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以外でも利用できるようになっています。