延迟初始化靠属性访问时计算并缓存结果,核心在__get__或@property逻辑;@property适用于单次无参只读场景,需用实例变量缓存;描述符可复用但须正确绑定实例;cached_property(Python 3.8+)线程安全、开箱即用;低开销或短生命周期对象不宜使用。

延迟初始化不是靠装饰器自动实现的,而是靠属性访问时才计算并缓存结果——关键在 __get__ 或 property 的逻辑控制。
用 @property 实现最简懒加载
适用于单次计算、无参数、后续只读的场景。首次访问触发计算,结果存在实例字典里,后续直接返回。
常见错误是把耗时操作写在 __init__ 里,导致构造对象就卡住;或者每次访问都重新计算,失去“懒”的意义。
- 必须用实例变量(如
self._expensive_result)缓存结果,不能只返回临时值 - 如果计算过程可能失败,建议在
property内加try/except,否则首次访问异常会反复抛出 - 不支持传参,也不适合需要重置的场景
class DataLoader:
def __init__(self, path):
self.path = path
self._data = None
<pre class="brush:php;toolbar:false;">@property
def data(self):
if self._data is None:
print("Loading from disk...")
self._data = load_from_file(self.path) # 假设这是个慢操作
return self._data
用描述符(Descriptor)封装可复用的懒加载逻辑
当你在多个类、多个属性上重复写 if self._x is None: ...,就应该抽成描述符。它天然解决“首次访问才执行 + 自动缓存”问题。
容易踩的坑是描述符没正确处理实例绑定:__get__ 必须检查 obj 是否为 None(类访问时返回描述符本身),且缓存必须存在实例字典中,不能存在描述符实例里。
- 缓存键建议用
(obj, self)或带前缀的属性名,避免命名冲突 - 如果被修饰的属性名和缓存名相同(如
data),需用object.__setattr__避免触发自身__set__ - 描述符无法直接用于类变量或
@classmethod
class LazyAttr:
def __init__(self, func):
self.func = func
self.name = func.__name__
<pre class="brush:php;toolbar:false;">def __get__(self, obj, cls):
if obj is None:
return self
value = self.func(obj)
object.__setattr__(obj, self.name, value) # 绕过 __set__
return value使用
class Config: @LazyAttr def db_url(self): return read_config("DB_URL") # 真实调用
用 functools.cached_property(Python 3.8+)
这是标准库提供的开箱即用方案,语义清晰、线程安全(内部用锁)、自动处理实例绑定,比手写描述符更稳。
注意它和普通 @property 的核心区别:值缓存在实例 __dict__ 中,且不可被外部赋值覆盖(设值会抛 AttributeError)。
- 仅限 Python 3.8 及以上;旧版本可用
backports.cached-property - 不支持带参数的初始化函数(比如
load(key)),只能是无参方法 - 缓存不可清除(除非手动删实例属性),不适合需要动态刷新的场景
from functools import cached_property
<p>class ApiClient:
def <strong>init</strong>(self, token):
self.token = token</p><pre class="brush:php;toolbar:false;">@cached_property
def session(self):
print("Creating session...")
return requests.Session()
什么时候不该用延迟初始化?
延迟初始化不是银弹。如果初始化成本极低(比如只是赋一个字面量),或对象生命周期很短(创建后立刻销毁),加懒加载反而增加判断开销和代码复杂度。
更隐蔽的问题是:它会让依赖关系变得隐式。调试时你可能发现某个属性“突然变慢”,却查不到初始化点在哪——因为触发时机藏在第一次 .attr 访问里,而不是构造函数中明确写出。
多线程环境下,若多个线程同时首次访问同一懒属性,cached_property 能保证只算一次,但自定义描述符或 @property 若没加锁,可能重复计算甚至引发竞态。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











