pythonで動的に生成したモジュールをキャッシュする方法

pythonで動的に生成したモジュールをキャッシュする方法

pythonで動的に作ったコードから生成したモジュールをキャッシュする方法について調べたのでまとめました。

  • bytecode経由でキャッシュする方法
  • tempfile経由でキャッシュする方法

ちなみにこれはテンプレートエンジンを作る時に調べました。前者はjinja2が使っていて、後者はmakoが使っている方法です。

今回は以下の様なコードをキャッシュすることにします(とある記事の答えです)。

def distribute(xs, n):
    return zip(*zip(*([iter(xs)] * n)))

bytecode経由でキャッシュする方法

こちらはpythonの.pycの形式に従った形でファイルを出力する方法です。.pyc形式に従っているのでimportで読み込めます。 (TODO:後で.pycの形式について解説する)

以下を実行すると「distribute.pyc」というファイルが生成されます。

import time
import marshal
import imp

source = """\
def distribute(xs, n):
    return zip(*zip(*([iter(xs)] * n)))
"""


def write_long(f, x):
    f.write(bytes([x & 0xff, (x >> 8) & 0xff, (x >> 16) & 0xff, (x >> 24) & 0xff]))


with open("distribute.pyc", "wb") as wf:
    timestamp = int(time.time())
    size = len(source)  # xxx:

    code = compile(source, "<string>", "exec")

    wf.write(b'\0\0\0\0')
    write_long(wf, timestamp)
    write_long(wf, size)
    marshal.dump(code, wf)
    wf.seek(0)
    wf.write(imp.get_magic())

実際にdistribute.pycを利用してみます。bytecodeを直接生成してあげればimportも可能ということです。 ちなみにjinja2ではこの通りのコードではないです。(jina2.bccacheの中を覗いてみてください)

$ ls | grep -F .pyc
distribute.pyc
$ python
Python 3.3.1 (default, Apr  6 2013, 12:29:05)
[GCC 4.2.1 Compatible Apple Clang 4.1 ((tags/Apple/clang-421.11.66))] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import distribute
>>> list(distribute.distribute(range(10), 3))
[(0, 3, 6), (1, 4, 7), (2, 5, 8)]

tempfile経由でキャッシュする方法

tempfile経由でキャッシュする方法は以下の様な感じです。昨日作った kamo ではこちらを使うことにしました。

import tempfile
import os
source = '''\
def distribute(xs, n):
    return zip(*zip(*([iter(xs)] * n)))
'''

fd, path = tempfile.mkstemp()
print(path) # /var/folders/b7/2rk7xp2d0hb2r21zbzjwxb_m0000gn/T/tmpgpfeaa
os.write(fd, source.encode("utf-8"))

実行した時に書き込まれた場所は「/var/folders/b7/2rk7xp2d0hb2r21zbzjwxb_m0000gn/T/tmpgpfeaa」でした。 こちらの場合は読み込む際にimportではなくimportlibの内部のapiを利用します。 (python2.xの場合にはmachineryではなくimp moduleを使った感じになるかもしれません。)

from importlib import machinery
target = "/var/folders/b7/2rk7xp2d0hb2r21zbzjwxb_m0000gn/T/tmpgpfeaa"
d = machinery.SourceFileLoader("d", target).load_module()

print(list(d.distribute(range(10), 3)))
# [(0, 3, 6), (1, 4, 7), (2, 5, 8)]