静かなる名辞

pythonと読書

【python】メモリ上のオブジェクトを是が非でも圧縮したい

 でかいデータをなにも考えずメモリ上に置いておくと、あっという間にメモリが埋まる。不要なデータはこまめに消して、必要なときに必要なものだけメモリに置くようにすれば大抵なんとかなるのだけど、そうやって整理していくと、ある水準を超えたところで処理時間とかコードの可読性が問題になってしまうときもある。
 こういう問題の解決策として、データを生成したあとは圧縮してメモリ上に置いておき、使うときに解凍して呼び出すという方法が考えられる。もちろん普通にできることではないので、pickleを使う。

 こういうのは説明するよりソースを見せた方が早い。

import pickle
import bz2
PROTOCOL = pickle.HIGHEST_PROTOCOL
class ZpkObj:
    def __init__(self, obj):
        self.zpk_object = bz2.compress(pickle.dumps(obj, PROTOCOL), 9)        

    def load(self):
        return pickle.loads(bz2.decompress(self.zpk_object))

 こういうファイルを作ってパスの通ったとこに置いておく。pickle化・圧縮のプロトコルは気分で変える。
 使い方も特に難しくはない。

from zpkobj import ZpkObj #zpkobjというファイル名にした場合
...
obj = ZpkObj(obj) #圧縮するとき
...
obj = obj.load() #解凍するとき

 留意点としては、オリジナルのオブジェクトの参照がどこかに残っているとGCが動かないので期待したようなメモリ節約効果が得られないことが挙げられる。そういうときは手動で消す。

...
zobj1 = ZpkObj(obj1)
del obj1 #違う名前に代入するなら必須

 せっかくなので、memory_profilerでベンチマークを取ってみる。

mtest.py(ベンチマーク用コード)

# coding: UTF-8
from zpkobj import ZpkObj

@profile
def main():
    lst = list(range(10000000))
    zlst = ZpkObj(lst)
    del lst
    lst = zlst.load()
    del zlst

if __name__ == '__main__':
    main()

結果

$ python -m memory_profiler mtest.py
Filename: mtest.py

Line #    Mem usage    Increment   Line Contents
================================================
     4   28.066 MiB    0.000 MiB   @profile
     5                             def main():
     6  415.867 MiB  387.801 MiB       lst = list(range(10000000))
     7  422.105 MiB    6.238 MiB       zlst = ZpkObj(lst)
     8   34.645 MiB -387.461 MiB       del lst
     9  428.691 MiB  394.047 MiB       lst = zlst.load()
    10  423.742 MiB   -4.949 MiB       del zlst

 7~8行目を見ればわかるように、意図した通りオブジェクトのサイズを1/50以下に圧縮できている(圧縮の効きやすさはデータ依存なんで、すべてのオブジェクトのかさを1/50以下にできる訳ではないけど)。
 ところで、6行目と10行目で若干メモリ消費量が増えてるのはなんで? memory_profilerの使用メモリが載っちゃってるか、それとも一千万個も生成した整数オブジェクトをぜんぶ回収しないうちに次の処理が進んでるんだろうか。そこに関しては詳しくないので謎。