method_cache

投稿者: ytyng 12年, 10ヶ月 前
# -*- 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)
#
現在未評価

コメント

アーカイブ

2024
2023
2022
2021
2020
2019
2018
2017
2016
2015
2014
2013
2012
2011