@cached_property和@lru_cache装饰器的区别

25

我是Django的新手。希望有人能告诉我@cached_property和@lru_cache装饰器在Django中的区别,以及在什么情况下应该使用哪个装饰器。使用案例会非常有帮助。谢谢。


请注意:如果想要更好地控制模型及其字段的缓存,请查看django-cacheops:https://github.com/Suor/django-cacheops/blob/master/README.rst - Sławomir Lenart
请注意 @cached_property,令人困惑的是,它实际上并不是一个缓存版本的@property,尽管名称暗示了这一点。https://bugs.python.org/issue41523 - lunguini
1
这个常见问题解答条目讨论了以下区别:https://docs.python.org/3/faq/programming.html#how-do-i-cache-method-calls - Raymond Hettinger
3个回答

30

首先,lru_cache 是Python自带的装饰器,从3.4版本开始提供;cached_property是Django多年来提供的装饰器,在2019年10月被添加到Python 3.8版本中。尽管如此,它们非常相似。

lru_cache 特别适用于函数式编程。它可以保存一组特定参数的函数调用结果。当装饰了 lru_cache 的函数被多次使用相同的参数调用时,该装饰器将返回一个缓存的函数结果。这使用了一种编程方法称为动态规划,更具体地说,是记忆化。使用这些方法,您可以极大地加速重复调用计算昂贵的函数的代码。

Python还提供了另一个类似的装饰器 called lfu_cache。这两个装饰器都实现了记忆化,但采用不同的替换策略lru_cache(最近最少使用)将填充缓存并在下一个装饰函数调用期间强制退出某些内容。替换策略规定最近最少使用的条目将被新数据替换。而lfu_cache(最不经常使用)则根据哪些条目最少使用来进行替换。

cached_propertylru_cache 相似,它们都会缓存调用昂贵函数的结果。唯一的区别在于 cached_property 只能用于方法,也就是属于对象的函数。而且,这些方法只能有 self 作为参数,不能有其他参数。在 django 开发中,应该特别使用这个方法来处理需要访问数据库的类方法。 在 Django 文档 中,提到了将其应用于模型类上的属性方法 friends,此方法预计会访问数据库以获取与该 Person 实例相对应的一组朋友。由于对数据库的调用很昂贵,因此我们希望将结果缓存以供以后使用。


5
cached_property 自 Python 3.8 起被包含在标准库中,它可以用来缓存类属性的值。 - Lord Elrond

12
  1. 一个主要的区别是lru_cache会保留缓存中的对象,这可能会导致内存泄露,特别是如果应用lru_cache的实例很大(参见:https://bugs.python.org/issue19859)。
class A:

  @property
  @functools.lru_cache(maxsize=None)
  def x(self):
    return 123

for _ in range(100):
  A().x  # Call lru_cache on 100 different `A` instances

# The instances of `A()` are never garbage-collected:
assert A.x.fget.cache_info().currsize == 100

使用 cached_property,没有缓存,所以也不会有内存泄漏问题。

class B:

  @functools.cached_property
  def x(self):
    return 123

b = B()
print(vars(b))  # {}
b.x
print(vars(b))  # {'x': 123}
del b  # b is garbage-collected
  1. 另一个区别是@property是只读的,而@cached_property不是。cache_property允许对属性进行写操作。查看Python文档
A().x = 123
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: can't set attribute

B().x = 123  # Works

这是因为 @cached_property 替换了属性,所以对 b.x 的第二次调用绕过了 B.x.get 描述符的调用。

  1. 另一个可能在大多数情况下并不重要的区别是,如果你多次访问同一属性,则 cached_propertylru_cache 更高效,而 lru_cache 对于函数调用和属性查找有开销。注意,只有在处理大量数据时才会看到明显的差异。
[A().x for _ in range(10_000)]
[B().x for _ in range(10_000)]

a = A()
b = B()

print(timeit.timeit(lambda: a.x, number=1_000_000))  # ~0.83
print(timeit.timeit(lambda: b.x, number=1_000_000))  # ~0.57

1
谢谢您提到 @cached_property 属性不是只读的,这是一个重要的细节,在 Python 文档 中奇怪地没有被强调。如果您问我,有点误导人... - lunguini
1
@lunguini Python文档明确提到cached_property允许对属性进行写操作。 来自Python文档的摘录: cached_property()的机制与property()有些不同。常规属性会阻止属性写入,除非定义了setter。相比之下,cached_property允许写入。 - PradeepK
你是对的,文档现在明确讨论了这个问题。在3.8版本中(包括@cached_property的第一个版本),他们没有这样做。 - lunguini

6
他们有不同的用途。 保存最近使用的内容——您需要指定,以区分您可以保存多少次函数计算结果。一旦超过这个数字,“最老”的结果将被丢弃,新的结果将被保存。 只计算并保存结果。与不同,它不需要参数(您可以将其视为一个对象类型的,maxsize = 1,没有参数)。

lru_cache(最大缓存大小为1)是否正确地等同于并可替代cached_property,只是cached_property可能更高效?这种差异的相关性有多大?对于Python < 3.8来说,lru_cache是适合模拟cached_property的备选方法吗? - mara004
1
不,大小为1的lru_cache与cached_property不同,因为lru_cache是一个装饰器,返回一个函数,而cached_property创建一个属性(不需要使用'()'来调用)。它们在某种程度上相似,即一旦计算完成,就不需要再次计算。 - undefined
不,大小为1的lru_cache与cached_property不同,因为lru_cache是一个装饰器,返回一个函数,而cached_property创建一个属性(不需要使用'()'来调用)。它们在某种程度上相似,即一旦计算完成,就不需要再次计算。 - Mariy
我明白了。如果在@lru_cache之上再添加一个额外的@property,效果是一样的吗? - mara004
我明白了。如果在@lru_cache上面再添加一个@property,效果是一样的吗? - undefined
cached_property的文档实际上建议将property与lru_cache堆叠使用作为一种替代方案,所以应该没问题吧? - mara004

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接