
本文探讨了在Python中如何高效管理对象中依赖于其他属性的昂贵计算属性,特别是在对象初始化期间避免不必要的计算和潜在错误。通过引入`functools.cached_property`,我们展示了一种优雅且Pythonic的方法,来自动缓存属性值,并在其依赖项发生变化时智能地使其失效,从而取代了手动管理初始化标志的复杂性,提升了代码的简洁性和性能。
在面向对象编程中,我们经常会遇到这样的场景:一个类的某个属性(例如,一个total值)是根据其其他几个属性(例如,apple和orange的数量)计算得出的。如果这个计算过程非常耗时,并且该属性会被频繁读取,那么为了性能考虑,我们通常会选择缓存这个计算结果,而不是每次访问时都重新计算。然而,当这些依赖属性发生变化时,缓存的值需要被更新。更具挑战性的是,在对象初始化阶段,依赖属性可能尚未完全设置,此时尝试计算缓存值可能会导致AttributeError。
考虑一个抽象容器Container和其具体实现Basket的例子。Basket包含多种水果,并有一个total属性表示水果的总量。这个total的计算成本较高,因此我们希望在水果数量变化时才重新计算,并且在初始化过程中避免因部分属性未定义而导致的错误。
原始的设计思路可能是在__setattr__方法中检查属性是否属于依赖项,并在属性发生变化时触发compute_total方法。为了避免在初始化期间因属性未完全设置而引发错误,可能需要引入一个手动管理的_initialised标志:
立即学习“Python免费学习笔记(深入)”;
from abc import ABC, abstractmethod
class Container(ABC):
_fruits = []
def __init__(self):
self._initialised = False # 手动初始化标志
def __setattr__(self, name, value):
super().__setattr__(name, value)
# 仅在初始化完成后且依赖属性变化时触发计算
if name in self._fruits and hasattr(self, 'compute_total') and self._initialised:
self.compute_total()
@abstractmethod
def compute_total(self):
pass
class Basket(Container):
_fruits = ['apple', 'orange']
def __init__(self, apple, orange, color):
super(Basket, self).__init__()
self.apple = apple
self.orange = orange
self.color = color
self._total = 0
self.compute_total() # 初始化时首次计算
self._initialised = True # 标记初始化完成
@property
def total(self):
return self._total
def compute_total(self):
self._total = self.apple + self.orange这种方法虽然解决了问题,但引入了一个额外的_initialised标志,增加了代码的复杂性,并且不够“Pythonic”。我们需要一种更简洁、更自动化的机制来处理这类场景。
Python 3.8 引入的 functools.cached_property 装饰器为解决此类问题提供了一个优雅的方案。cached_property 类似于 property,但它会缓存其计算结果。首次访问时,被装饰的方法会被调用并计算出结果,然后该结果会被存储在实例的 __dict__ 中,后续访问将直接返回缓存值,而不再执行方法。
当依赖属性发生变化时,我们只需从实例的 __dict__ 中删除 cached_property 对应的键,即可使其缓存失效。下一次访问该属性时,cached_property 将重新计算并缓存新值。
在 Container.__setattr__ 中使缓存失效: 当任何被 _fruits 列表标记为依赖项的属性被设置时,我们尝试从实例的 __dict__ 中删除 total 属性。如果 total 尚未被访问过(即尚未缓存),则 KeyError 会被捕获并忽略,这正是我们期望的行为。
在 Basket 中使用 @cached_property: 将 total 方法标记为 @cached_property。这样,total 的计算只会在首次访问时发生,或者在缓存被手动删除后再次访问时发生。
下面是使用 cached_property 优化后的代码实现:
from abc import ABC
from functools import cached_property
class Container(ABC):
_fruits = []
def __setattr__(self, name, value):
super().__setattr__(name, value)
# 如果设置的属性是依赖项,则尝试使 'total' 缓存失效
if name in self._fruits:
try:
del self.__dict__['total']
except KeyError:
# 'total' 尚未被访问或缓存,无需处理
pass
class Basket(Container):
_fruits = ['apple', 'orange']
def __init__(self, apple, orange, color):
super(Basket, self).__init__()
self.apple = apple
self.orange = orange
self.color = color
@cached_property
def total(self):
"""
计算并缓存水果总数。
这个方法只会在首次访问或缓存失效后被调用。
"""
print("Calculating total...") # 用于演示何时进行计算
out = self.apple + self.orange
return out
让我们通过一个简单的交互式会话来验证上述实现的行为:
# 创建一个Basket实例
b = Basket(apple=10, orange=5, color='red')
# 首次访问total,会触发计算
print(f"Initial total: {b.total}") # 输出: Calculating total... \n Initial total: 15
# 再次访问total,直接返回缓存值,不会触发计算
print(f"Second access total: {b.total}") # 输出: Second access total: 15
# 改变一个依赖属性 (apple)
b.apple = 12
# 此时,__setattr__会删除b.__dict__['total'],使缓存失效
# 再次访问total,会重新触发计算
print(f"Total after changing apple: {b.total}") # 输出: Calculating total... \n Total after changing apple: 17
# 改变另一个依赖属性 (orange)
b.orange = 8
# 再次访问total,会重新触发计算
print(f"Total after changing orange: {b.total}") # 输出: Calculating total... \n Total after changing orange: 20
# 改变一个非依赖属性 (color)
b.color = 'green'
# 再次访问total,不会触发计算,因为color不是_fruits中的依赖项
print(f"Total after changing color: {b.total}") # 输出: Total after changing color: 20从上面的输出可以看出,total属性的计算只在必要时发生,即首次访问或其依赖项改变后。在对象初始化期间,由于total属性在__init__中没有被访问,所以不会在依赖属性尚未完全设置时尝试计算,从而避免了潜在的AttributeError。
通过 functools.cached_property,我们能够以一种更加声明式和Pythonic的方式来管理昂贵计算属性的缓存和更新逻辑,避免了手动管理状态标志的复杂性,提高了代码的可读性和维护性。这种模式在处理数据模型中派生属性时尤其有用,能够有效地平衡性能与代码简洁性。
以上就是如何优雅地管理Python中昂贵计算属性的缓存与更新的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号