我可以像这样缓存数据类的实例属性:
from dataclasses import dataclass
from functools import cached_property
@dataclass
class Point:
_x: float
@cached_property
def x(self):
return self._x * 2
>> p = Point(3)
>> p.x
6
我希望每次调用它时都将计算应用于
x
,使用缓存的结果进行后续调用以避免不必要的计算。
但我也想要改变
x
虽然
p.x += 1
适用于上面的代码,但很多地方都说
@cached_property
适用于不可变值。在我实际的、更复杂的代码中,它给了我奇怪的、难以理解的结果。我将此归因于当装饰器与可变属性一起使用时令人惊讶的行为(即使我不完全理解发生了什么)。
缓存在其整个生命周期中发生变化的实例属性的正确方法是什么?
你对
@cached_property
和可变性的担心是完全有道理的。尽管你的例子可以运行,但它依赖于微妙且容易出错的行为。让我们来分析一下问题所在,并探讨一些更强大的解决方案。
@cached_property
问题
@cached_property
的设计目的是用于计算结果仅依赖于不可变输入的属性。它通过在第一次调用时缓存属性的结果来工作。后续调用不会重新计算结果,而是返回缓存值。
在你的
Point
类中,
x
依赖于
_x
。当执行
p.x += 1
时,实际上并没有改变
_x
;只是修改了缓存的
x
属性。这可能会导致与
_x
的实际值不一致,并且在更大的代码库中,这种不一致可能会变得难以跟踪。
缓存可变属性的解决方案
以下是缓存可变属性的一些更可靠的方法:
- 显式缓存管理:
与其依赖
@cached_property
,不如在需要时手动管理缓存。这可以通过在类中引入一个单独的缓存字典来完成:
```python class Point: def init (self, x): self._x = x self._cache = {}
def x(self):
if 'x' not in self._cache:
self._cache['x'] = self._x * 2
return self._cache['x']
def update_x(self, new_x):
self._x = new_x
self._cache.pop('x', None) # 使缓存失效
p = Point(3) print(p.x()) # 计算并缓存 print(p.x()) # 从缓存返回
p.update_x(5) print(p.x()) # 重新计算并缓存 ```
在这个例子中,
x()
方法检查缓存中是否存在结果。如果存在,则返回缓存值。否则,将计算该值,将其存储在缓存中,然后返回。
update_x()
方法确保在
_x
发生更改时使缓存失效。
-
使用
lru_cache
(对于计算成本高的属性):
如果计算属性的成本很高,并且希望缓存最近的结果,则可以使用
functools
模块中的
lru_cache
装饰器。
lru_cache
实现了 LRU(最近最少使用)缓存机制。
```python from functools import lru_cache
class Point: def init (self, x): self._x = x
@lru_cache(maxsize=None)
def x(self):
return self._x * 2
p = Point(3) print(p.x()) print(p.x()) # 从缓存返回 ```
请注意,
lru_cache
不会自动使属性的缓存失效。需要在修改
_x
时手动使缓存失效(例如,使用与之前类似的
update_x
方法)。
- 重新设计以获得不变性(如果可能):
如果可能,最好的解决方案是重新设计你的类以使用不可变数据结构。这将消除对缓存可变属性的需求,并使你的代码更易于推理。
请记住,为可变属性选择正确的缓存策略取决于你的具体需求和代码的复杂性。
标签:python,properties,memoization From: 78841994