Python: 属性字段是否会自动缓存?

36

我的问题是以下两段代码是否由解释器运行相同:

class A(object):
  def __init__(self):
     self.__x = None

  @property
  def x(self):
     if not self.__x:
        self.__x = ... #some complicated action
     return self.__x

而且更简单的方法是:

class A(object):
  @property
  def x(self):
      return ... #some complicated action

也就是说,解释器是否足够聪明,能够缓存属性x

我的假设是,x的值不会改变 - 找到它是困难的,但一旦找到了它,就没有再次查找的理由。

8个回答

35

不,每次访问属性时都会调用 getter。


23

19

不,你需要添加一个memoize装饰器:

class memoized(object):
   """Decorator that caches a function's return value each time it is called.
   If called later with the same arguments, the cached value is returned, and
   not re-evaluated.
   """
   def __init__(self, func):
      self.func = func
      self.cache = {}
   def __call__(self, *args):
      try:
         return self.cache[args]
      except KeyError:
         value = self.func(*args)
         self.cache[args] = value
         return value
      except TypeError:
         # uncachable -- for instance, passing a list as an argument.
         # Better to not cache than to blow up entirely.
         return self.func(*args)
   def __repr__(self):
      """Return the function's docstring."""
      return self.func.__doc__
   def __get__(self, obj, objtype):
      """Support instance methods."""
      return functools.partial(self.__call__, obj)

@memoized
def fibonacci(n):
   "Return the nth fibonacci number."
   if n in (0, 1):
      return n
   return fibonacci(n-1) + fibonacci(n-2)

print fibonacci(12)

7
对于一个简单的 Python 属性来说,memoize 装饰器可能有些过度设计。 - SingleNegationElimination
3
请注意,同时存在 functools.lru_cache(maxsize=128, typed=False) 装饰器(也允许 maxsize=None 参数)。 - Tobias Kienzler

17

属性不会自动缓存其返回值。getter(和setter)的目的是每次访问属性时都要调用。

然而,Denis Otkidach编写了一个精彩的缓存属性装饰器(在《Python Cookbook》第二版上发布,最初在ActiveState上以PSF许可证发布),专门用于此目的:

class cache(object):    
    '''Computes attribute value and caches it in the instance.
    Python Cookbook (Denis Otkidach) https://stackoverflow.com/users/168352/denis-otkidach
    This decorator allows you to create a property which can be computed once and
    accessed many times. Sort of like memoization.

    '''
    def __init__(self, method, name=None):
        # record the unbound-method and the name
        self.method = method
        self.name = name or method.__name__
        self.__doc__ = method.__doc__
    def __get__(self, inst, cls):
        # self: <__main__.cache object at 0xb781340c>
        # inst: <__main__.Foo object at 0xb781348c>
        # cls: <class '__main__.Foo'>       
        if inst is None:
            # instance attribute accessed on class, return self
            # You get here if you write `Foo.bar`
            return self
        # compute, cache and return the instance's attribute value
        result = self.method(inst)
        # setattr redefines the instance's attribute so this doesn't get called again
        setattr(inst, self.name, result)
        return result

这里有一个演示其使用的例子:
def demo_cache():
    class Foo(object):
        @cache
        def bar(self):
            print 'Calculating self.bar'  
            return 42
    foo=Foo()
    print(foo.bar)
    # Calculating self.bar
    # 42
    print(foo.bar)    
    # 42
    foo.bar=1
    print(foo.bar)
    # 1
    print(Foo.bar)
    # __get__ called with inst = None
    # <__main__.cache object at 0xb7709b4c>

    # Deleting `foo.bar` from `foo.__dict__` re-exposes the property defined in `Foo`.
    # Thus, calling `foo.bar` again recalculates the value again.
    del foo.bar
    print(foo.bar)
    # Calculating self.bar
    # 42

demo_cache()

15

在Django中也可以使用from django.utils.functional import cached_property - Seb D.
1
我不知道为什么你的答案没有得到更多的赞。对我来说,在用@property修饰函数之前,使用@functools.lru_cache(1)修饰函数可以起到很好的作用。 - pepoluan
只是为了让其他人明白:在使用@property修饰函数之前,先使用@functools.lru_cache(1)来修饰函数意味着@functools.lru_cache(1)要放在@property之后(即最靠近函数)。 - user118967

5

我必须查找了一下,因为我也有这个问题。

标准库中的functools包将会增加一个cached_property装饰器。不幸的是,它只在Python 3.8中可用(截至本帖发表时,为3.8a0)。等待的替代方案是使用自定义的装饰器,例如0xc0de提到的这个或Django的,然后稍后切换:

from django.utils.functional import cached_property
# from functools import cached_property # Only 3.8+ :(

2
你知道吗?你可以堆叠 @property@functools.lru_cache(maxsize)。而且自 Python 3.2 起,@functools.lru_cache 就可用了。 - pepoluan

3
Denis Otkidach提到的装饰器在@unutbu的答案中提到,已经发表在O'Reilly的Python Cookbook中。不幸的是,O'Reilly没有为代码示例指定任何许可证 - 只是非正式地允许重用该代码。
如果您需要一个具有自由许可证的缓存属性装饰器,可以使用Ken Seehof的@cached_property,它在ActiveState代码配方中明确发布在MIT许可下。
def cached_property(f):
    """returns a cached property that is calculated by function f"""
    def get(self):
        try:
            return self._property_cache[f]
        except AttributeError:
            self._property_cache = {}
            x = self._property_cache[f] = f(self)
            return x
        except KeyError:
            x = self._property_cache[f] = f(self)
            return x

    return property(get)

2
此外,如果您正在开发Django项目,可以从django.utils.functional导入@cached_property - Amir Rustamzadeh

1
注意:为了完整性,这里添加了一些可用选项。
不,property 默认情况下不会被缓存。但是有几种选项可以实现这种行为,我想再添加一种选项:

https://github.com/pydanny/cached-property


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接