web-dev-qa-db-ja.com

最初のアクセス後に戻り値をキャッシュするクラスメソッドのデコレータ

私の問題とその理由

クラスメソッド@cachedpropertyのデコレータを作成しようとしています。メソッドが最初に呼び出されたときに、メソッドがその戻り値で置き換えられるように動作させる必要があります。また、@propertyのように動作させて、明示的に呼び出す必要がないようにしたいです。基本的に、それは@propertyを除いて区別できないはずです値を一度計算してから保存するだけなので、より高速です。これは、__init__だと思います。だから、私はこれをやりたいと思っています。

私が試したもの

最初に、fgetpropertyメソッドをオーバーライドしようとしましたが、これは読み取り専用です。

次に、最初に呼び出す必要があるが値をキャッシュするデコレータを実装しようと考えました。これは、決して呼び出す必要のないプロパティタイプのデコレータの最終目標ではありませんが、これは最初に取り組む方が簡単な問題だと思いました。言い換えれば、これは少し単純な問題に対しては機能しない解決策です。

私は試した:

def cachedproperty(func):
    """ Used on methods to convert them to methods that replace themselves 
        with their return value once they are called. """
    def cache(*args):
        self = args[0] # Reference to the class who owns the method
        funcname = inspect.stack()[0][3] # Name of the function, so that it can be overridden.
        setattr(self, funcname, func()) # Replace the function with its value
        return func() # Return the result of the function
    return cache

しかし、これはうまくいかないようです。私はこれをテストしました:

>>> class Test:
...     @cachedproperty
...     def test(self):
...             print "Execute"
...             return "Return"
... 
>>> Test.test
<unbound method Test.cache>
>>> Test.test()

しかし、クラスがそれ自体をメソッドに渡さなかったというエラーが表示されます。

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unbound method cache() must be called with Test instance as first argument (got nothing instead)

この時点で、私と深いPythonメソッドについての限られた知識は非常に混乱しており、コードがどこで間違っているのか、どのように修正するのかわかりません(私はこれまで書いたことはありません)前のデコレータ)

質問

@propertyのように)最初にクラスメソッドを呼び出した結果を返し、以降のすべてのクエリでキャッシュされた値に置き換えるデコレーターを作成するにはどうすればよいですか?

この質問があまり混乱しないことを願っています。できるだけ説明しました。

26
Luke Taylor

まず、Testをインスタンス化する必要があります

_test = Test()
_

2番目に、inspectは必要ありません。これは、_func.__name___からプロパティ名を取得できるためです。3番目に、property(cache)を返してpythonすべての魔法を実行します。

_def cachedproperty(func):
    " Used on methods to convert them to methods that replace themselves\
        with their return value once they are called. "

    def cache(*args):
        self = args[0] # Reference to the class who owns the method
        funcname = func.__name__
        ret_value = func(self)
        setattr(self, funcname, ret_value) # Replace the function with its value
        return ret_value # Return the result of the function

    return property(cache)


class Test:
    @cachedproperty
    def test(self):
            print "Execute"
            return "Return"

>>> test = Test()
>>> test.test
Execute
'Return'
>>> test.test
'Return'
>>>
_

「」

8
robyschek

別の解決策を気にしないのであれば、私はお勧めします lru_cache

例えば

from functools import lru_cache
class Test:
    @property
    @lru_cache(maxsize=None)
    def calc(self):
        print("Calculating")
        return 1

期待される出力

In [2]: t = Test()

In [3]: t.calc
Calculating
Out[3]: 1

In [4]: t.calc
Out[4]: 1
16
00500005

これはまさに記述子の種類なので、カスタム記述子を使用したほうがよいと思います。そのようです:

class CachedProperty:
    def __init__(self, name, get_the_value):
        self.name = name
        self.get_the_value = get_the_value
    def __get__(self, obj, typ): 
        name = self.name
        while True:
            try:
                return getattr(obj, name)
            except AttributeError:
                get_the_value = self.get_the_value
                try:
                    # get_the_value can be a string which is the name of an obj method
                    value = getattr(obj, get_the_value)()
                except AttributeError:
                    # or it can be another external function
                    value = get_the_value()
                setattr(obj, name, value)
                continue
            break


class Mine:
    cached_property = CachedProperty("_cached_property ", get_cached_property_value)

# OR: 

class Mine:
    cached_property = CachedProperty("_cached_property", "get_cached_property_value")
    def get_cached_property_value(self):
        return "the_value"

編集:ところで、実際にはカスタム記述子も必要ありません。プロパティ関数内の値をキャッシュするだけで済みます。例えば。:

@property
def test(self):
    while True:
        try:
            return self._test
        except AttributeError:
            self._test = get_initial_value()

これですべてです。

しかし、多くの人はこれをpropertyの乱用であり、予期しない使用方法であると考えています。そして通常、予期しないことは、それを別のより明確な方法で行う必要があることを意味します。カスタムCachedProperty記述子は非常に明示的であるため、より多くのコードが必要ですが、そのため、propertyアプローチよりも優先します。

3
Rick Teachey

次のようなものを使用できます。

def cached(timeout=None):
    def decorator(func):
        def wrapper(self, *args, **kwargs):
            value = None
            key = '_'.join([type(self).__name__, str(self.id) if hasattr(self, 'id') else '', func.__name__])

            if settings.CACHING_ENABLED:
                value = cache.get(key)

            if value is None:
                value = func(self, *args, **kwargs)

                if settings.CACHING_ENABLED:
                    # if timeout=None Django cache reads a global value from settings
                    cache.set(key, value, timeout=timeout)

            return value

        return wrapper

    return decorator

キャッシュディクショナリに追加すると、エンティティをキャッシュしていて、プロパティがエンティティごとに異なる値を返す可能性がある場合に備えて、class_id_functionの規則に基づいてキーを生成します。

また、ベンチマークを行うときに一時的にオフにしたい場合に備えて、設定キーCACHING_ENABLEDもチェックします。

ただし、標準のpropertyデコレータはカプセル化されないため、関数のように呼び出す必要があります。または、次のように使用できます(プロパティに限定する理由)。

@cached
@property
def total_sales(self):
    # Some calculations here...
    pass

また、遅延外部キー関係の結果をキャッシュしている場合、データによっては、選択クエリを実行して一度にすべてをフェッチするよりも、単に集計関数を実行した方が速い場合があることに注意する必要があります。結果セットのすべてのレコードのキャッシュにアクセスします。したがって、フレームワークにDjango-debug-toolbarなどのツールを使用して、シナリオで最もパフォーマンスが高いものを比較します。

2
fips

Djangoのこのデコレータのバージョンは、あなたが説明したことを正確に実行し、シンプルなので、私のコメントに加えて、ここにコピーします。

class cached_property(object):
    """
    Decorator that converts a method with a single self argument into a
    property cached on the instance.

    Optional ``name`` argument allows you to make cached properties of other
    methods. (e.g.  url = cached_property(get_absolute_url, name='url') )
    """
    def __init__(self, func, name=None):
        self.func = func
        self.__doc__ = getattr(func, '__doc__')
        self.name = name or func.__name__

    def __get__(self, instance, type=None):
        if instance is None:
            return self
        res = instance.__dict__[self.name] = self.func(instance)
        return res

ソース )。

ご覧のように、それはfunc.nameを使用して関数の名前を決定し(inspect.stackをいじる必要はありません)、メソッドをinstance.__dict__。したがって、後続の「呼び出し」は単なる属性ルックアップであり、キャッシュなどは必要ありません。

1
RemcoGerlich