装饰器,用于在首次访问后缓存返回值的类方法

时间:2016-04-18 01:55:49

标签: python caching decorator python-decorators

我的问题,为什么

我正在尝试为类方法@cachedproperty编写一个装饰器。我希望它的行为,以便在首次调用该方法时,该方法将替换为其返回值。我还希望它的行为类似于@property,因此不需要显式调用它。基本上,它应该与@property无法区分,除了它更快,因为它只计算一次值然后存储它。我的想法是,这不会减慢实例化速度,例如在__init__中定义它。这就是我想要这样做的原因。

我尝试了什么

首先,我尝试覆盖fget的{​​{1}}方法,但它是只读的。

接下来,我想我会尝试实现一个需要第一次调用的装饰器,然后缓存值。这不是我永远不需要调用的属性类型装饰器的最终目标,但我认为这将是一个更容易解决的问题。换句话说,对于稍微简单的问题,这是一个无法解决的问题。

我试过了:

property

然而,这似乎不起作用。我测试了这个:

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()

此时,我和我对深度Python方法的有限知识非常困惑,我不知道我的代码出错了或者如何修复它。 (我之前从未尝试过写装饰师)

问题

如何编写一个装饰器,它将在第一次访问时返回调用类方法的结果(如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) 那样),并替换为所有后续查询的缓存值?

我希望这个问题不会太混乱,我试着尽可能地解释它。

6 个答案:

答案 0 :(得分:15)

如果您不介意其他解决方案,我建议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

答案 1 :(得分:8)

首先应该Test实例化

test = Test()

其次,不需要inspect因为我们可以从func.__name__获取属性名称 第三,我们返回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'
>>>

“”“

答案 2 :(得分:3)

我认为你最好使用自定义描述符,因为这正是描述符的用途。像这样:

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 :(得分:2)

您可以使用以下内容:

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

另外值得注意的是,如果你是从懒惰的外键关系缓存结果,有时候根据你的数据,在执行select查询并获取所有内容时简单地运行聚合函数会更快一次,而不是访问结果集中每条记录的缓存。因此,使用django-debug-toolbar之类的工具为您的框架比较在您的方案中表现最佳的工具。

答案 4 :(得分:1)

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

source)。

正如您所看到的,它使用func.name来确定函数的名称(不需要使用inspect.stack),并通过变更instance.__dict__将结果替换为方法。因此,后续的“调用”只是一个属性查找,不需要任何缓存,等等。

答案 5 :(得分:0)

@functools.lru_cache()
def func(....):
    ....

参考:@functools.lru_cache() | Python