我正在编写一个Python类,其中有一个属性需要相对较长的时间来计算,因此我只想计算一次。此外,并非每个类实例都需要该属性,因此在 __init__
中不想默认计算。
我是Python新手,但并非编程新手。我可以轻松地想出一种方法来解决这个问题,但我发现一次又一次的,以“Pythonic”方式处理事情通常比我用其他语言的经验设计的方式要简单得多。
在Python中有一种“正确”的做法吗?
我正在编写一个Python类,其中有一个属性需要相对较长的时间来计算,因此我只想计算一次。此外,并非每个类实例都需要该属性,因此在 __init__
中不想默认计算。
我是Python新手,但并非编程新手。我可以轻松地想出一种方法来解决这个问题,但我发现一次又一次的,以“Pythonic”方式处理事情通常比我用其他语言的经验设计的方式要简单得多。
在Python中有一种“正确”的做法吗?
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
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)
或者使用其他答案中提到的适合您需求的实现方式。
或者使用上述提到的后移版。
lru_cache
的默认大小为 128,这会导致属性函数可能被调用两次。如果您使用 lru_cache(None)
,所有实例将永久保持活动状态。 - orlplru_cache
默认大小为 128,用于处理 128 种不同的参数配置。只有当你生成的对象超过缓存大小时才会出现问题,因为这里唯一会发生变化的参数是 self。如果你正在生成如此多的对象,那么你真的不应该使用无限制的缓存,因为它将强制你无限期地保留所有调用该属性的对象,这可能会导致可怕的内存泄漏。无论如何,你可能最好使用一种在对象本身中存储缓存的缓存方法,这样缓存就会随对象一起清除。 - Taywee@property @functools.lru_cache()
方法给我一个 TypeError: unhashable type
错误,可能是因为 self
不可哈希。 - Daniel Himmelsteinfunctools.lru_cache
似乎会导致类的实例在缓存中时避免垃圾回收。更好的解决方案是Python 3.8中的functools.cached_property
。 - user1338062@property
和@functools.lru_cache
合并成了@cached_property
"有点误导人。[这个答案演示了两者如何合并] (https://dev59.com/IWQo5IYBdhLWcg3wXuin#16099881)但是如果你尝试设置新值而没有编写setter,它不会像`@cachedproperty`那样运行。在您的两个示例代码片段中,尝试`x=MyClass()`,`print(x.foo)`,`x.foo=6*9`以查看区别。从文档中可以看到:“ cached_property()
的机制与property()
略有不同。常规属性阻止属性写入,除非定义了setter。相反,cached_property允许写入。” - Silverfish我曾经按照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.
cached_property
的包,其中包括线程安全和按时间过期的版本。(同时感谢 @Florian 提供的解释。) - leewz __slots__
时无法使用 cached_property
描述符。__slots__
使用数据描述符实现,而使用 cached_property
描述符会简单地覆盖生成的 slot 描述符,因此 setattr()
调用将无效,因为没有 __dict__
可以设置属性,而此属性名称可用的唯一描述符是 cached_property
。只是为了帮助其他人避免这种陷阱,将其放在这里。 - Martijn Pieterscached_property包
来完成任务。 - Ruben Alves通常的方法是将属性设为属性,并在第一次计算时存储其值。
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进行装饰更符合Pythonic风格。 - Brad Solomon@functools.lru_cache()
会将结果以self
参数为键进行缓存,这也会防止该实例在缓存中存在时被垃圾回收。 - rectalogicPython 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__
的类,除非在 __slots__
中定义了 __dict__
。
如前所述,functools.cached_property
可用于缓存实例属性。对于缓存的类属性:
from functools import cache
class MyClass:
@classmethod
@property
@cache
def foo(cls):
print('expensive calculation')
return 42
>>> MyClass.foo
expensive calculation
42
>>> MyClass.foo
42
如果您想要一个可重复使用的装饰器:
def cached_class_attr(f):
return classmethod(property(cache(f)))
class MyClass:
@cached_class_attr
def foo(cls):
...
Python版本必须在3.9及以上且小于3.11
这里要介绍的是dickens
包(不是我的),它提供了cachedproperty
, classproperty
和cachedclassproperty
修饰器。
如果要缓存类属性,可以使用以下方式:
from descriptors import cachedclassproperty
class MyClass:
@cachedclassproperty
def approx_pi(cls):
return 22 / 7
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})
mapping = {}
class A:
def __init__(self):
if self.__class__.__name__ not in mapping:
print('Expansive calculation')
mapping[self.__class__.__name__] = self.__class__.__name__
self.cached = mapping[self.__class__.__name__]
为了说明:
foo = A()
bar = A()
print(foo.cached, bar.cached)
提供
Expansive calculation
A A
你可以尝试使用记忆化技术。它的工作原理是,如果你传入相同的参数给一个函数,它将返回缓存的结果。你可以在这里找到更多关于如何在Python中实现它的信息。
此外,根据你的代码设置方式(你说并非所有实例都需要它),你可以尝试使用某种轻量级模式,或者延迟加载。
Foo.something_expensive
。所有这些答案都是关于缓存的_instance_属性,这意味着something_expensive
将为每个新实例重新计算,这在大多数情况下都不是最佳选择。 - steve@classmethod
进行包装,这应该会为您提供一个缓存的类属性。 - Jake Stevens-Haas@classmethod
和@functools.cached_property
两个装饰器来创建缓存的类属性,无论顺序如何都无法实现。相比之下,Dickens库中的@cachedclassproperty
装饰器对我起作用了。 - nimble_ninja