讲师中心 微信公众号
AI工具推荐 视频效率加速

如何优雅地管理Python中昂贵计算属性的缓存与更新

大晨君_9764

大晨君_9764

发布时间:2025-11-18 12:10:02

|

770人浏览过

|

来源于php中文网

原创

如何优雅地管理python中昂贵计算属性的缓存与更新

本文探讨了在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学习
python学习

Python 编程学习助手 skill,面向零基础用户。 当用户表达以下意图时触发: - 学习 Python 基础概念(变量、数据类型、循环、函数、类等) - 请求 Python 练习题或测验 - 请求审查或调试 Python 代码 - 请求 Python 项目实战指导(爬虫、数据分析、Web 开发等入门项目) - 泛化表述如"教我 Python""我想学编程""Python 怎么写""帮我学 Python" 此 skill

下载

解决方案:利用 functools.cached_property

Python 3.8 引入的 functools.cached_property 装饰器为解决此类问题提供了一个优雅的方案。cached_property 类似于 property,但它会缓存其计算结果。首次访问时,被装饰的方法会被调用并计算出结果,然后该结果会被存储在实例的 __dict__ 中,后续访问将直接返回缓存值,而不再执行方法。

当依赖属性发生变化时,我们只需从实例的 __dict__ 中删除 cached_property 对应的键,即可使其缓存失效。下一次访问该属性时,cached_property 将重新计算并缓存新值。

实现细节

  1. 在 Container.__setattr__ 中使缓存失效: 当任何被 _fruits 列表标记为依赖项的属性被设置时,我们尝试从实例的 __dict__ 中删除 total 属性。如果 total 尚未被访问过(即尚未缓存),则 KeyError 会被捕获并忽略,这正是我们期望的行为。

  2. 在 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。

注意事项与总结

  1. 适用场景: cached_property 最适合那些计算成本高昂、结果相对稳定、且被频繁读取的属性。如果属性值频繁变化,或者计算成本很低,直接使用普通 @property 可能更简单。
  2. 缓存位置: cached_property 将计算结果存储在实例的 __dict__ 中。这意味着每个实例都有自己的缓存,并且缓存值会覆盖同名的描述符对象。
  3. 缓存失效: 使 cached_property 缓存失效的唯一方法是从实例的 __dict__ 中删除对应的键。
  4. 线程安全: cached_property 在并发访问时不是线程安全的。如果多个线程可能同时首次访问或在缓存失效后访问同一个 cached_property,可能会导致多次计算。在这种情况下,需要额外的同步机制。

通过 functools.cached_property,我们能够以一种更加声明式和Pythonic的方式来管理昂贵计算属性的缓存和更新逻辑,避免了手动管理状态标志的复杂性,提高了代码的可读性和维护性。这种模式在处理数据模型中派生属性时尤其有用,能够有效地平衡性能与代码简洁性。

热门AI工具

更多
Seko
Seko Hot

一款AI视频创作工具,主要用于商汤科技推出的创编一体的AI短视频创作Agent,适合需要提升相关任务效率的用户。

二狗PPT
二狗PPT Hot

一款AI演示文稿工具,主要用于专为中式职场打造的AI PPT生成工具,适合需要提升相关任务效率的用户。

豆包大模型

豆包大模型是一款由字节跳动推出的企业级大语言模型服务平台。

DeepSeek

DeepSeek是一款面向对话、写作、编程和推理场景的AI大模型工具。

火山引擎

火山引擎是一款面向企业的云计算与AI服务平台。

SkildArt
SkildArt Hot

SkildArt是一款AI文本写作工具,一站式 AI 视觉创作平台。

PixPix
PixPix Hot

PixPix是一款面向电商视觉生产的AI商品图生成工具。

VibeKnow
VibeKnow Hot

一款AI视频创作工具,主要用于全球首个AI知识视频创作平台,文档、文章、网页,一键生成视频,适合需要提升相关任务效率的用户。

WorkBuddy

一款AI办公效率工具,主要用于腾讯云推出的AI原生桌面智能体工作台,适合需要提升相关任务效率的用户。

相关专题

更多
go语言 面向对象
go语言 面向对象

本专题整合了go语言面向对象相关内容,阅读专题下面的文章了解更多详细内容。

6595

2025.09.05

java面向对象
java面向对象

本专题整合了java面向对象相关内容,阅读专题下面的文章了解更多详细内容。

342

2025.11.27

C++ 面向对象编程与设计模式实践
C++ 面向对象编程与设计模式实践

本专题整合了 C++ 面向对象编程的核心机制,涵盖类的定义与访问控制、构造函数与析构函数、拷贝语义与移动语义、继承与虚函数多态、抽象类与接口、运算符重载、友元函数等关键知识点,同时结合单例模式、工厂模式、观察者模式、策略模式等常用设计模式的 C++ 实现,帮助开发者写出结构清晰、易于维护的面向对象代码。

344

2026.04.09

Java 面向对象与核心编程机制
Java 面向对象与核心编程机制

系统讲解 Java 面向对象编程的核心知识体系,涵盖类的定义与封装、构造方法与 this/super 关键字、继承与方法重写、多态与向上向下转型、抽象类与接口的区别与设计原则、异常处理机制(try-catch-finally / 自定义异常)、泛型的类型参数与通配符、集合框架(List/Set/Map)的使用与遍历方式,帮助开发者全面掌握 Java 程序设计的核心机制与面向对象思维。

223

2026.04.17

线程和进程的区别
线程和进程的区别

线程和进程的区别:线程是进程的一部分,用于实现并发和并行操作,而线程共享进程的资源,通信更方便快捷,切换开销较小。本专题为大家提供线程和进程区别相关的各种文章、以及下载和课程。

4258

2023.08.10

PHP 命令行脚本与自动化任务开发
PHP 命令行脚本与自动化任务开发

本专题系统讲解 PHP 在命令行环境(CLI)下的开发与应用,内容涵盖 PHP CLI 基础、参数解析、文件与目录操作、日志输出、异常处理,以及与 Linux 定时任务(Cron)的结合使用。通过实战示例,帮助开发者掌握使用 PHP 构建 自动化脚本、批处理工具与后台任务程序 的能力。

486

2025.12.13

Figma AI自动化智能数据填充与交互生成实战
Figma AI自动化智能数据填充与交互生成实战

告别机械重复,详细演示如何用 AI 填充业务真实数据,并自动为页面添加交互连线,将静态设计稿快速转变为动态原型。

325

2026.05.13

剧云故事大纲与分场规划教程
剧云故事大纲与分场规划教程

本专题讲解剧云梗概与多级大纲功能,覆盖全局、分集、分场结构生成,以及主线冲突、剧情节点和场次顺序调整。

0

2026.10.22

VibeKnow PPT讲解视频教程汇总
VibeKnow PPT讲解视频教程汇总

本专题介绍VibeKnow PPT讲解模式的素材准备、旁白生成、页面节奏、画面呈现与导出设置,适合课程和培训场景。

0

2026.10.22

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Uvicorn官方安装与入门手册
Uvicorn官方安装与入门手册

共0课时 | 0人学习

Python uv官方项目管理指南
Python uv官方项目管理指南

共0课时 | 0人学习

Python uv官方安装与入门文档
Python uv官方安装与入门文档

共0课时 | 0人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn