在Python中缓存类属性
我正在用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)
或者你可以参考其他回答中提到的任何适合你需求的实现。
或者使用上面提到的解决方案。