在首次访问后缓存返回值的方法的修饰器



我的问题,以及为什么

我正在尝试为一种方法编写一个装饰器,@cachedproperty.我希望它的行为使得当第一次调用该方法时,该方法被替换为它的返回值。我还希望它的行为像@property这样就不需要显式调用它。基本上,它应该与@property没有区别,除了它更快,因为它只计算一次值然后存储它。我的想法是,这不会像__init__定义实例化那样减慢实例化的速度。这就是我想这样做的原因。

我尝试了什么

首先,我尝试覆盖propertyfget方法,但它是只读的。

接下来,我想我会尝试实现一个装饰器,它确实需要在第一次调用,但随后缓存值。这不是我永远不需要调用的属性类型装饰器的最终目标,但我认为这将是一个需要首先解决的更简单的问题。换句话说,这是一个稍微简单的问题的行不通的解决方案。

我试过了:

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一样(,并替换为所有后续查询的缓存值?

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

如果您

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

首先Test应该实例化

test = Test()

其次,没有必要inspect因为我们可以从func.__name__中获取属性名称第三,我们回归property(cache)让蟒蛇做所有的魔法。

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'
>>>

"">

在 Python 3.8 或更高版本中,您可以使用 functools.cached_property((。

它的工作原理类似于之前提出的lru_cache解决方案。

用法示例:

import functools
class Test:
    @functools.cached_property
    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

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

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方法,尽管它需要更多的代码。

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__ 将方法替换为其结果。所以后续的"调用"只是一个属性查找,不需要任何缓存等。

你可以使用这样的东西:

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(来比较方案中性能最佳的工具。

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

参考: @functools.lru_cache(( |蟒

你有没有尝试过内置的djangos:来自 django.utils.functional import cached_property

请不要按照多人的建议使用lru_cache,因为它会打开许多可能的内存泄漏问题

最新更新