l'essentiel est invisible pour les yeux

Thursday, September 11, 2008

Pythonでデコ•メソッドキャッシュ (memcached)

おっPythonハカーを多数抱える、煩悩駆動開発で有名なglucose.jpのお手伝いをする機会があり、4年ぶりにPythonを書いた。2.3以来だったので、2.5を眺めているとデコレーターと呼ばれる機能が導入されていた。

デコレーターとクロージャーを組み合わせれば、メソッドの挙動を自由にカスタマイズすることができる。この機能を使用して、関数の結果をmemcachedでキャッシュする機能を付与するデコレーターを作ってみた。


あらかじめ、CACHE変数をmemcachedクライアントで初期化しておく必要がある。

挙動
ソースは、少々ややこしいけど、実行の仕組みは単純。

Step 1:
@cached構文実行時に、ack関数に対して、cached関数が実行され、ack関数がdecorated_funcを呼び出した結果でack関数が置換される。(返り値は、callableなオブジェクトでないといけない。)

Step 2:
decorated_func関数の呼び出し時の引数は、元のack関数。decorated_func関数の内部では、func_with_cacheが定義されており、ack関数に渡した引数を渡して実行される。この関数が、関数呼び出しの結果のキャッシュをチェックする本体となる。


# Decorate methods with capability for caching result
#
# @author Rakuto Furutani <http://raku.to/>
# @version 0.1.0
#
import cPickle
import memcache
import time
import md5

__all__ = ['cached', 'init']

# TODO: CACHE variable and function for initialize move to scope in Devkit.cache.*
# You must define global variable named 'CACHE' with instance of memcache client.
CACHE = None

def init(cache_client):
""" We should initialize this library with this method
examples:
CACHE = memcache.Client(['127.0.0.1:11211'], debug=0)
methods_cache.init(CACHE)
"""
global CACHE
CACHE = cache_client

def cached(d_args):
""" Decorate method with caching capability for caching
All result call the function are cached with cache key generated by function name and parameters.

examples:
@cached({'ttl': 3600})
def havy_complex_func(args):
# do something

# Return whether result call real function or cached result
havy_complex_func()

# You can specify cache key name explicitly
@cached({'ttl': 3600, 'key': 'foo_bar'})
def foo_bar():
# doo something

# You can also decorate one without ttl parameter
@cached
def havy_complex_func(args):
# do something
"""
def cache_key(func, args):
"""Generate unique cache key with funcion name and arguments"""
return "%s_%s" % (func.__name__, md5.new(str(args)).hexdigest())

def call_and_cache(func, args, key):
ret = func(*args)
data = {'ttl': (time.time() + d_args['ttl'] if 'ttl' in d_args else None), 'd': ret}
CACHE.set(key, cPickle.dumps(data))
return ret

def decorated_func(func):
def func_with_cache(*args):
key = d_args['key'] if ('key' in d_args) else cache_key(func, args)
cached_val = CACHE.get(key)
if cached_val is not None:
cached_val = cPickle.loads(cached_val)
if (cached_val['ttl'] is not None) and (cached_val['ttl'] >= time.time()):
return cached_val['d']
return call_and_cache(func, args, key)
return func_with_cache
return decorated_func

if __name__ == '__main__':
import sys
import time

sys.setrecursionlimit(10000)

# Define memcache client
CACHE = memcache.Client(['127.0.0.1:11211'], debug=0)

def _internal_ack(m, n):
if m == 0:
result = n + 1
elif n == 0:
result = ack(m - 1, 1)
else:
result = ack(m - 1, ack(m, n -1))
return result

def ack(m, n):
return _internal_ack(m, n)

@cached({'ttl': 3600})
def ack_with_cache(m, n):
return _internal_ack(m, n)

t = time.time()
ret = ack(3, 4)
print("ack(3, 4) returned %d - %s sec" % (ret, str(time.time() - t)))

ack_with_cache(3, 4) # cached once
t = time.time()
ret = ack_with_cache(3, 4)
print("ack(3, 4) returned %d with cached - %s sec" % (ret, str(time.time() - t)))


実行結果

ack(3, 4) returned 125 - 0.00920796394348 sec
ack(3, 4) returned 125 with cached - 0.000248908996582 sec


まとめ
来月から、shn@glucose.jpによるおっPython連載がオライリーで始まるらしい。代表取締役としての世間体と、煩悩の狭間で、どこまで書いていいのか悩んでいるようだ。結論をそれに帰着するという路線と合わせて、”ゆるかわPython”なるコンセプトを提案し、20代後半女性Pythonianを増やす革命者になるという路線も考えているようだ。

記事の影響により、glucose発の女性Pythonハカー誕生なるか。