我正在用python写一个类,并且有一个属性,该属性将花费相对较长的时间来计算,因此我只想执行一次。此外,它会不会被类的每个实例需要的,所以我不想在默认情况下做到这一点在__init__
。
我是Python的新手,但不是编程人员。我可以想出一种很容易做到这一点的方法,但是我一遍又一遍地发现,“ Pythonic”做事的方法通常比我在其他语言中的经验要简单得多。
在Python中是否有“正确”的方法来做到这一点?
Answers:
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)
或者其他提及的任何实现都可以满足您的需求。
或上述反向端口。
lru_cache
的默认大小为128,导致该属性函数可能被调用两次。如果要使用,lru_cache(None)
所有实例将永久保留。
@property @functools.lru_cache()
方法给我一个TypeError: unhashable type
错误,大概是因为self
不可散列。
functools.lru_cache
只要类实例在缓存中,它们就可以避免GC。更好的解决方案是functools.cached_property
在Python 3.8中。
我过去经常这样做,但我最终还是厌倦了小小的整理工作。
所以我建立了自己的描述符:
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.
cached_property
PyPI上有一个软件包。它包括线程安全和过期版本。(还要感谢@Florian的解释。)
cached_property
时不能使用描述符__slots__
。插槽是使用数据描述符实现的,并且使用cached_property
描述符仅覆盖生成的插槽描述符,因此该setattr()
调用将无法进行,因为没有__dict__
设置属性的属性,并且该属性名称唯一可用的描述符是cached_property
..在这里帮助其他人避免这种陷阱。
通常的方法是使属性成为属性,并在首次计算时存储该值
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
@property + @functools.lru_cache()
?准私有属性的方式似乎让人想起Java / setters / getters。在我的拙见中,仅使用lru_cache进行装饰更具有Python风格
@functools.lru_cache()
将缓存用self
arg键键入的结果,这也将防止该实例只要在缓存中就被GC。
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__
。
您可以尝试研究记忆。它的工作方式是,如果在函数中传递相同的参数,它将返回缓存的结果。您可以在此处找到有关在python中实现它的更多信息。
另外,根据代码的设置方式(您说并非所有实例都需要此代码),您可以尝试使用某种flyweight模式或延迟加载。
Foo.something_expensive
。所有这些答案都与缓存的实例属性有关,这意味着something_expensive
将为每个新实例重新计算,这在大多数情况下都不是最佳选择