Python中类的"缓存"属性

mwo*_*e02 47 python memoization

我在python中编写一个类,我有一个属性需要相对长的时间来计算,所以我只想做一次.此外,它会不会被类的每个实例需要的,所以我不想在默认情况下做到这一点__init__.

我是Python新手,但不是编程.我可以很容易地找到一种方法来做到这一点,但我一次又一次地发现,使用我在其他语言中的经验,"Pythonic"做事的方式通常比我想象的要简单得多.

在Python中有没有"正确"的方法呢?

Max*_* R. 56

Python≥3.2

你应该使用both @property@functools.lru_cache装饰器:

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

这个答案有更详细的例子,并且还提到了以前Python版本的backport.

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)
Run Code Online (Sandbox Code Playgroud)

或者在其他答案中提到的任何实现符合您的需求.
或者上面提到的backport.

  • 小心!在我看来,只要类实例位于缓存中,“functools.lru_cache”就会导致类实例避免 GC。更好的解决方案是 Python 3.8 中的“functools.cached_property”。 (6认同)
  • lru_cache也被移植到python 2:https://pypi.python.org/pypi/functools32/3.2.3 (3认同)
  • 对于128种不同的参数配置,@ orlp lru_cache的默认大小为128.如果您生成的对象多于缓存大小,这只会是一个问题,因为这里唯一不断变化的参数是self.如果你生成这么多对象,你真的不应该使用无界缓存,因为它会强制你将所有曾经无限期地调用该属性的对象保留在内存中,这可能是一个可怕的内存泄漏.无论如何,使用缓存在对象本身中存储缓存的方法可能会更好,因此缓存会被清除. (3认同)
  • `@property @functools.lru_cache()` 方法给了我一个 `TypeError: unhashable type` 错误,大概是因为 `self` 不可散列。 (2认同)

Jon*_*ric 45

我曾经这样做过gnibbler的建议,但我最终厌倦了小家务.

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

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
Run Code Online (Sandbox Code Playgroud)

这是你如何使用它:

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.
Run Code Online (Sandbox Code Playgroud)

  • 精彩!以下是它的工作原理:实例变量[优先于非数据描述符](https://docs.python.org/2/howto/descriptor.html#descriptor-protocol).在第一次访问属性时,没有实例属性,只有描述符类属性,因此执行描述符.但是,在执行期间,描述符会创建一个具有缓存值的实例属性.这意味着当第二次访问属性时,将返回先前创建的实例属性,而不是正在执行的描述符. (11认同)
  • 对于深奥的角落案例:使用`__slots__`时不能使用`cached_property`描述符.使用数据描述符实现插槽,并使用`cached_property`描述符简单地覆盖生成的插槽描述符,因此`setattr()`调用将不起作用,因为没有`__dict__`来设置属性并且唯一的描述符可用对于这个属性名称是`cached_property` ..只是把它放在这里帮助其他人避免这个陷阱. (4认同)
  • PyPI上有一个[`cached_property`包](https://pypi.python.org/pypi/cached-property).它包括线程安全和时间过期版本.(另外,谢谢@Florian的解释.) (2认同)

Joh*_*ooy 37

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

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
Run Code Online (Sandbox Code Playgroud)

  • @Brad`@ functools.lru_cache()`将缓存使用`self` arg键输入的结果,这也将防止该实例只要在缓存中就被GC。 (3认同)
  • 在Python3.2 +中,有没有动机使用这种方法而不是`@property + @ functools.lru_cache()`?准私有属性方式似乎让人联想到Java/setters/getters; 在我的拙见中,用lru_cache进行装饰更具有pythonic (2认同)

Sup*_*oot 20

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)
Run Code Online (Sandbox Code Playgroud)

的限制是,与属性的对象要被缓存必须有一个__dict__属性,该属性是一个可变的映射,以排除类__slots__除非__dict__中定义__slots__


aja*_*nss 14

如前所述,functools.cached_property适用于缓存的实例属性。对于缓存的属性:

3.9 <= 蟒蛇 < 3.13

from functools import cache

class MyClass:
    @classmethod
    @property
    @cache  # or lru_cache() for python < 3.9
    def foo(cls):
        print('expensive calculation')
        return 42
Run Code Online (Sandbox Code Playgroud)
>>> MyClass.foo
expensive calculation
42
>>> MyClass.foo
42
Run Code Online (Sandbox Code Playgroud)

如果你想要一个可重复使用的装饰器:

def cached_class_attr(f):
    return classmethod(property(cache(f)))

class MyClass:
    @cached_class_attr
    def foo(cls):
        ...
Run Code Online (Sandbox Code Playgroud)

蟒蛇 >= 3.13

在 3.13 中,链接classmethodproperty是不允许的,因此您必须使用元类或自定义装饰器,以下是只读缓存属性的示例:

class MyMeta(type):
    @property
    @cache
    def foo(self):
        ...

class MyClass(metaclass=MyMeta):
    ...

MyClass.foo  # read-only access
Run Code Online (Sandbox Code Playgroud)

或者自定义装饰器:

class classproperty:
    def __init__(self, func) -> None:
        functools.update_wrapper(self, func)
    def __get__(self, instance, owner):
        return self.__wrapped__(owner)

class MyClass:
    @classproperty
    @cache
    def foo(cls):
        ...
Run Code Online (Sandbox Code Playgroud)

  • 只花了 12 年时间和其他 10 个(高票数)答案,有人终于给出了正确的答案。谢谢 (6认同)

Acu*_*nus 5

dickens包(不是我的)提供cachedproperty,classpropertycachedclassproperty装饰器。

缓存类属性

from descriptors import cachedclassproperty

class MyClass:
    @cachedclassproperty
    def approx_pi(cls):
        return 22 / 7
Run Code Online (Sandbox Code Playgroud)