在Python中缓存类属性


76

我正在用python写一个类,并且有一个属性,该属性将花费相对较长的时间来计算,因此我只想执行一次。此外,它会不会被类的每个实例需要的,所以我不想在默认情况下做到这一点__init__

我是Python的新手,但不是编程人员。我可以想出一种很容易做到这一点的方法,但是我一遍又一遍地发现,“ Pythonic”做事的方法通常比我在其他语言中的经验要简单得多。

在Python中是否有“正确”的方法来做到这一点?


5
IMO这些答案都不正确。OP想要一个缓存的属性,例如Foo.something_expensive。所有这些答案都与缓存的实例属性有关,这意味着something_expensive将为每个新实例重新计算,这在大多数情况下都不是最佳选择
steve

Answers:


86

Python≥3.8 @property@functools.lru_cache已合并为@cached_property

import functools
class MyClass:
    @functools.cached_property
    def foo(self):
        print("long calculation here")
        return 21 * 2

Python≥3.2 <3.8

您应该同时使用@property@functools.lru_cache装饰器:

import functools
class MyClass:
    @property
    @functools.lru_cache()
    def foo(self):
        print("long calculation here")
        return 21 * 2

该答案有更详细的示例,还提到了先前Python版本的反向移植。

Python <3.2

Python Wiki具有一个缓存的属性装饰器(由MIT许可),可以这样使用:

import random
# the class containing the property must be a new-style class
class MyClass(object):
   # create property whose value is cached for ten minutes
   @cached_property(ttl=600)
   def randint(self):
       # will only be evaluated every 10 min. at maximum.
       return random.randint(0, 100)

或者其他提及的任何实现都可以满足您的需求。
或上述反向端口。


3
lru_cache也已反向移植到python 2:pypi.python.org/pypi/functools32/3.2.3
Buttons840 2014年

1
-1lru_cache的默认大小为128,导致该属性函数可能被调用两次。如果要使用,lru_cache(None)所有实例将永久保留。
orlp 2015年

3
@orlp lru_cache的默认大小为128,用于128个不同的参数配置。仅当生成的对象超出缓存大小时,这才是问题,因为这里唯一变化的参数是self。如果生成的对象太多,则实际上不应该使用无限制的缓存,因为它将迫使您将所有曾经调用过该属性的对象无限期地保留在内存中,这可能是可怕的内存泄漏。无论如何,使用缓存方法将缓存存储在对象本身中可能会更好,因此可以使用它来清理缓存。
塔威

1
@property @functools.lru_cache()方法给我一个TypeError: unhashable type错误,大概是因为self不可散列。
丹尼尔·希默尔斯泰因

6
小心!在我看来,functools.lru_cache只要类实例在缓存中,它们就可以避免GC。更好的解决方案是functools.cached_property在Python 3.8中。
user1338062

49

我过去经常这样做,但我最终还是厌倦了小小的整理工作。

所以我建立了自己的描述符:

class cached_property(object):
    """
    Descriptor (non-data) for building an attribute on-demand on first use.
    """
    def __init__(self, factory):
        """
        <factory> is called such: factory(instance) to build the attribute.
        """
        self._attr_name = factory.__name__
        self._factory = factory

    def __get__(self, instance, owner):
        # Build the attribute.
        attr = self._factory(instance)

        # Cache the value; hide ourselves.
        setattr(instance, self._attr_name, attr)

        return attr

使用方法如下:

class Spam(object):

    @cached_property
    def eggs(self):
        print 'long calculation here'
        return 6*2

s = Spam()
s.eggs      # Calculates the value.
s.eggs      # Uses cached value.

12
精彩!它是这样工作的:实例变量优先于非数据描述符。在属性的第一次访问中,没有实例属性,只有描述符类属性,因此执行了描述符。但是,描述符在其执行期间会使用缓存的值创建一个实例属性。这意味着当第二次访问该属性时,将返回先前创建的实例属性,而不是执行描述符。
Florian Brucker 2014年

3
cached_propertyPyPI上有一个软件包。它包括线程安全和过期版本。(还要感谢@Florian的解释。)
leewz

5
对于深奥的极端情况,是的:使用cached_property时不能使用描述符__slots__。插槽是使用数据描述符实现的,并且使用cached_property描述符仅覆盖生成的插槽描述符,因此该setattr()调用将无法进行,因为没有__dict__设置属性的属性,并且该属性名称唯一可用的描述符是cached_property..在这里帮助其他人避免这种陷阱。
马丁·彼得斯

38

通常的方法是使属性成为属性,并在首次计算时存储该值

import time

class Foo(object):
    def __init__(self):
        self._bar = None

    @property
    def bar(self):
        if self._bar is None:
            print "starting long calculation"
            time.sleep(5)
            self._bar = 2*2
            print "finished long caclulation"
        return self._bar

foo=Foo()
print "Accessing foo.bar"
print foo.bar
print "Accessing foo.bar"
print foo.bar

3
在Python3.2 +中,是否有动机使用此方法@property + @functools.lru_cache()?准私有属性的方式似乎让人想起Java / setters / getters。在我的拙见中,仅使用lru_cache进行装饰更具有Python风格
Brad Solomon

(如@Maxime的回答
Brad Solomon

4
@Brad@functools.lru_cache()将缓存用selfarg键键入的结果,这也将防止该实例只要在缓存中就被GC。
rectalogic

18

Python 3.8包含functools.cached_property装饰器。

将类的方法转换为属性,该属性的值将被计算一次,然后在实例生命周期中作为常规属性进行缓存。与相似property(),但增加了缓存。对于实例有效的不可变的昂贵的计算属性很有用。

这个例子直接来自文档:

from functools import cached_property

class DataSet:
    def __init__(self, sequence_of_numbers):
        self._data = sequence_of_numbers

    @cached_property
    def stdev(self):
        return statistics.stdev(self._data)

    @cached_property
    def variance(self):
        return statistics.variance(self._data)

限制是具有要缓存的属性的对象必须具有__dict__可变映射的属性,__slots__除非__dict__定义了,否则排除了类__slots__


2
class MemoizeTest:

      _cache = {}
      def __init__(self, a):
          if a in MemoizeTest._cache:
              self.a = MemoizeTest._cache[a]
          else:
              self.a = a**5000
              MemoizeTest._cache.update({a:self.a})

2

dickens软件包(不是我的)提议cachedpropertyclasspropertycachedclassproperty装饰。

要缓存类属性

from descriptors import cachedclassproperty

class MyClass:
    @cachedclassproperty
    def approx_pi(cls):
        return 22 / 7

1

您可以尝试研究记忆。它的工作方式是,如果在函数中传递相同的参数,它将返回缓存的结果。您可以在此处找到有关在python中实现它的更多信息。

另外,根据代码的设置方式(您说并非所有实例都需要此代码),您可以尝试使用某种flyweight模式或延迟加载。


-3

执行此操作的最简单方法可能是编写环绕该属性的方法(而不是使用属性)(getter方法)。在第一次调用时,此方法计算,保存并返回值;之后,它仅返回保存的值。


-4

使用Python 2,但不使用Python 3,这是我要做的。这与您获得的效率差不多:

class X:
    @property
    def foo(self):
        r = 33
        self.foo = r
        return r

说明:基本上,我只是用计算值重载属性方法。因此,在您第一次访问该属性(对于该实例)之后,foo不再是一个属性,而是成为一个实例属性。这种方法的优点是缓存命中尽可能便宜,因为self.__dict__,并且如果不使用该属性,则不会产生实例开销。

这种方法不适用于Python 3。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.