method_cache — Django でクラスメソッドの結果をキャッシュするデコレータ
Django
2011-06-05 12:09 (15 years ago)

# -*- coding:utf-8 -*-
"""
クラスメソッド結果をキャッシュするデコレータ
"""
import hashlib
import base64
from functools import wraps
import logging
from django.core.cache import cache as django_cache
from django.utils.encoding import smart_str
def _logging_verbose(message):
#pass
logging.debug('[METHOD_CACHE] %s' % message)
def _execute(method, obj, args, kwargs, cache_timeout=None, cache_backend=None):
"""
メソッドを実行する
実行結果がキャッシュされていれば、実行せずにそれを返す
"""
cache_key = _generate_cache_key(obj, method, args, kwargs)
if cache_backend is None:
cache_backend = django_cache
result = cache_backend.get(cache_key, None)
if result is None:
#メソッド実施!
result = method(obj, *args, **kwargs)
if cache_timeout is None:
cache_backend.set(cache_key, result)
else:
cache_backend.set(cache_key, result, cache_timeout)
_logging_verbose('cache NO hit. key=%s' % cache_key)
else:
_logging_verbose('cache hit. key=%s' % cache_key)
return result
def _generate_cache_key(obj, method, args=[], kwargs={}):
"""
キャッシュのキーを作成
オブジェクト名, メソッド名, 引数を連結して作成。
250文字を超えそうだったら、引数をハッシュ化する
@param
(class|instance) obj: クラスかインスタンス
(function|str) method: メソッド自体か、そのメソッドの名前の文字列
(list) args: 引数リスト
(dict) kw: キーワード引数のディクショナリ
"""
KEY_LENGTH_LIMIT = 250
o_name = obj.__name__ if hasattr(obj,'__name__') else obj.__class__.__name__
if hasattr(method, '_original_method'):
method = method._original_method
f_name = method.func_name
arg_names = method.func_code.co_varnames[1:method.func_code.co_argcount]
#↑引数の名前のリスト0には cls か self が入ってくるので見ない
arg_nv_list = []
for i in range(len(arg_names)):
arg_name = arg_names[i]
if len(args) > i:
arg_nv_list.append((arg_name, args[i]))
elif arg_name in kwargs:
arg_nv_list.append((arg_name, kwargs[arg_name]))
else:
arg_nv_list.append((arg_name, ''))
args_str = ','.join([ repr(arg_name) + ":" + _get_arg_value_unique_name(arg_value) for arg_name, arg_value in arg_nv_list ])
approach_1 = "MC/%s/%s/%s" % (o_name, f_name, args_str)
if len(approach_1) < KEY_LENGTH_LIMIT:
return approach_1
else:
# キーが長すぎたので一部ハッシュ化
hashed = hashlib.md5(args_str)
hashed_key_part = base64.b64encode(hashed.digest())
approach_2 = "MC/%s/%s/*%s" % (o_name, f_name, hashed_key_part)
return approach_2[:KEY_LENGTH_LIMIT]
def _get_arg_value_unique_name(arg_value):
"""
arg_value を、他のと区別できるような文字列を作る
キャッシュのキーに使うため
"""
if hasattr(arg_value, 'pk'):
return str(arg_value.pk)
else:
return smart_str(arg_value, errors='ignore') #reprのほうがいい?
def method_cache(*args, **kwargs):
"""
デコレータ本体
基本的にクラスメソッドに使う。
インスタンスメソッドでも使えるが、インスタンスが異なっても同じキャッシュのキーなので、
期待してない結果が返ると思うので基本的に使わない。
第一引数にクラスかインスタンスがくることを想定しているのでstaticmethodでは使えない。
デコレータの一番下に書くこと推奨。(そうしないと対象メソッドの引数名が正しくとれない。多分。)
from common.decorators.method_cache import method_cache, delete_method_cache
class Hoge(object):
@classmethod
@method_cache
def heavy_method(cls, arg1):
...
...
def save(self):
delete_method_cache(self, self.heavy_method, args=(self.arg1,))
...
...
"""
cache_timeout = kwargs.pop('cache_timeout', None)
cache_backend = kwargs.pop('cache_backend', None)
assert not kwargs, "Keyword argument accepted is cache_backend or cache_timeout"
if cache_backend is None and cache_timeout is None:
method = args[0]
@wraps(method)
def decorate(obj, *args, **kwargs):
return _execute(method, obj, args, kwargs)
decorate._original_method = method #デコレートされててもメソッド引数が参照できるように
return decorate
else:
params = {}
if not cache_timeout is None:
params['cache_timeout'] = cache_timeout
if not cache_backend is None:
params['cache_backend'] = cache_backend
def _internal_params(method):
@wraps(method)
def decorate(obj, *args, **kwargs):
return _execute(method, obj, args, kwargs, **params)
decorate._original_method = method #デコレートされててもメソッド引数が参照できるように
return decorate
return _internal_params
def delete_method_cache(obj, method, args=[], kwargs={}, cache_backend=None):
"""
キャッシュを削除。
各モデルの save() なんかに仕込む。
パラメータは、名前なし引数でも名前付き引数でもらっても大丈夫
"""
cache_key = _generate_cache_key(obj, method, args=args, kwargs=kwargs)
if cache_backend is None:
cache_backend = django_cache
cache_backend.delete(cache_key)
_logging_verbose('cache delete. key=%s' % cache_key)
#
# サンプル
#
#class MethodCacheModel(models.Model):
# """
# method_cache デコレータを使うときには、必ず消すメソッドを各必要がある。
# 拡張先でdelete_cacheメソッドをオーバーライドすることで対応する。
# """
# class Meta:
# abstract = True
#
# def save(self, *args, **kwargs):
# super(MethodCacheModel, self).save(*args, **kwargs)
# self.delete_cache()
#
# def delete(self, *args, **kwargs):
# self.delete_cache()
# super(MethodCacheModel, self).delete(*args, **kwargs)
#
# def delete_cache(self):
# super(MethodCacheModel, self).delete_cache()
# delete_method_cache(self, self.get, args=(self.pk, ))
# delete_method_cache(self, self.get_all)
#
# @classmethod
# @method_cache
# def get_all(cls):
# return list(cls.objects.all())
#
# @classmethod
# @method_cache
# def get(cls, pk):
# return cls.objects.get(pk = pk)
#
評価をお願いします (会員登録・ログイン不要)
まだ評価がありません
著者は、アプリケーション開発会社 Cyberneura を運営しています。
開発相談をお待ちしています。
開発相談をお待ちしています。