在Python中缓存类属性

139 投票
11 回答
80534 浏览
提问于 2025-04-16 06:11

我正在用Python写一个类,这个类里有一个属性计算起来比较耗时,所以我只想计算一次。而且,并不是每个这个类的实例都需要这个属性,所以我不想在__init__里默认计算它

我对Python还不太熟,但编程方面有一些经验。我能想到一个比较简单的方法来实现这个,但我发现,很多时候用“Pythonic”的方式来做事情,往往比我用其他语言的经验想到的方法要简单得多。

在Python里,有没有一种“正确”的方法来做到这一点呢?

11 个回答

43

通常的做法是把这个属性变成一个属性,然后在第一次计算的时候把值存起来。

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
60

我以前是按照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

这是你可以使用它的方式:

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.
195

3.8 及以上版本的 Python

在这个版本中,@property@functools.lru_cache 这两个功能合并成了一个新的功能 @cached_property

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

3.2 ≤ Python < 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 的维基上有一个 缓存属性装饰器(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)

或者你可以参考其他回答中提到的任何适合你需求的实现。
或者使用上面提到的解决方案。

撰写回答