【问题标题】:Decorator for a class method that caches return value after first access第一次访问后缓存返回值的类方法的装饰器
【发布时间】:2016-08-09 14:53:55
【问题描述】:

我的问题,为什么

我正在尝试为类方法 @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 所做的那样),并为所有后续查询替换为缓存值?

我希望这个问题不会太令人困惑,我试图尽可能地解释它。

【问题讨论】:

  • 为什么不创建一个自定义描述符来返回缓存对象...?装饰器可以工作,但对于您正在尝试做的事情来说,它似乎是错误的工具。
  • 您希望这是一个 instance 属性还是 class 属性? Test.test 将是类属性的语法,但听起来您希望这是一个实例。
  • 请注意,Django 附带了这样一个装饰器,它的来源在这里:docs.djangoproject.com/en/1.9/_modules/django/utils/functional/…。它会在第一次调用 get() 时覆盖 instance.__dict__,以便值可以覆盖属性。
  • FTR:这种缓存方法第一次调用后返回值的原理叫做memoization
  • @oliver:但这比单纯的记忆更进一步,因为该方法实际上被值替换作为优化。这也意味着该方法不能接受任何参数,否则它将不起作用。

标签: python caching decorator python-decorators


【解决方案1】:

对于 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

【讨论】:

    【解决方案2】:
    @functools.lru_cache()
    def func(....):
        ....
    

    参考:@functools.lru_cache() | Python

    【讨论】:

      【解决方案3】:

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

      【讨论】:

      • 这比他描述的要慢,这仍然调用一个函数并在第一次之后每次访问该属性时进行缓存查找。
      • @RemcoGerlich 是的,但通常这是过早的优化。计算属性的高成本通常是性能的主要成本,例如,如果计算属性达到 IO。使用内置 python 执行此操作的能力意味着您几乎总是应该先尝试一下,看看它是否能解决您的问题。
      • 即使您的答案比 OP 描述的解决方案慢(并且更复杂,因为它涉及引入缓存),您也可以从“如果它只是您关心的性能”开始您的答案。将我的评论称为“过早” imo 有点奇怪,您根本不回答这个问题。
      • @RemcoGerlich OP 正在询问如何延迟缓存返回值。 OP 将store return value as an attribute 作为他们尝试的解决方案。 store return value as an attribute 是一个公平的答案,但其他解决 OP 问题的解决方案也是如此,并且与问题 A decorator for a class method that caches the return after first run
      • @RemcoGerlich 但您对性能的看法是正确的,因此进行了编辑以删除“如果您关心的只是性能”,因为它具有误导性。
      【解决方案4】:

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

      【讨论】:

      • 这真是太好了。谢谢你。我不知道 Django 实现了这种东西,我以为它只是 web 框架的东西。我会记住 Django 有一些有用的功能以供将来参考:)
      • 事实证明,在编写 web 框架的东西时,你经常需要一些方便的工具 :-)
      【解决方案5】:

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

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

      【讨论】:

      • 他当然可以,但他为什么要这样做?该属性应该只有一个责任,那就是返回应该的。
      • @fips 点。我不确定这里是否有正确答案,但我肯定看到这样的论点,即自定义描述符清楚地表明了要做什么,而以这种方式使用 property 有点滥用/扩展其目的。
      • 没错,描述符似乎是一种可行的方法,特别是如果您需要对实例进行操作或使用继承来以可重用的方式扩展它的功能。
      • @fips 是的,继承问题可能使描述符更有意义。
      【解决方案6】:

      首先应该实例化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'
      >>>
      

      """

      【讨论】:

      • 接受这个,因为它接近我的原始代码。这向我展示了我做错了什么,而不仅仅是替代解决方案。我绝对感谢所有其他答案,我会把它们都放在手边。
      • 我们怎样才能适应新式的课程呢?如果我有从对象派生的测试,我会使用上面的代码得到“AttributeError:无法设置属性”。我猜在新式类中,方法是只读的?
      • Audrey "cookiecutter" Greenfeld 的丈夫 Danny "Two Scoops" 为此编写了一个 Python 包:github.com/pydanny/cached-property
      • 这个解决方案似乎只适用于 python 2。我收到 python 3.8 的 AttributeError: can't set attribute 错误。
      【解决方案7】:

      你可以这样使用:

      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 之类的工具来比较在您的场景中表现最佳的工具。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-06-26
        • 2017-06-16
        • 1970-01-01
        • 1970-01-01
        • 2013-02-20
        • 2018-06-16
        相关资源
        最近更新 更多