类属性在内存中只有一份,所有实例共享同一可变对象;实例未覆写时通过类字典查找,原地修改会全局生效;修复需在__init__中初始化可变对象。

类属性在内存中只有一份对象
Python 的类属性不是“复制”给每个实例的,而是所有实例共享同一个内存地址。当你写 class A: data = [],这个 data 列表对象在类创建时就生成了,且仅此一份;所有 A() 实例访问 self.data 时,底层拿到的都是这个列表的引用。
- 用
id(A.data)和id(a1.data)、id(a2.data)对比,结果完全一致 - 对它做原地修改(如
append()、extend()、update())不会改变引用,只是改内容 - 所以只要没被实例变量遮蔽,所有实例看到的都是同一块内存里的最新状态
实例没覆写时,属性查找自动回退到类
Python 查找属性时走的是“实例字典 → 类字典”链。如果实例的 __dict__ 里没有该属性名,就会去类的 __dict__ 里找——而类字典里的那个可变对象,就是所有未覆写实例共用的源头。
-
a1.data.append(1):a1 没有data实例属性,查到类的data,然后调用它的append -
print(a2.data):a2 同样没data实例属性,查到同一个类data,自然看到[1] - 只有当你执行
a1.data = [99],才真正在 a1 的__dict__里写入新键值,完成遮蔽
不可变类型看起来“不共享”,其实是遮蔽生效
你写 class B: flag = 42,再执行 b1.flag += 1,b2.flag 还是 42 —— 这不是因为“不共享”,而是因为整数不可变,+= 实际触发了赋值操作:Python 自动做了 b1.flag = b1.flag + 1,也就是新建一个整数对象并绑定到 b1.__dict__['flag'] 上。
-
str、int、tuple等不可变类型,任何“修改”本质都是重新赋值 - 可变类型(
list、dict、set)的就地方法(append、clear、add)不换引用,所以穿透力强 - 判断依据不是“类型声明”,而是“是否调用了原地方法”或“是否用了
=赋值”
修复方式必须切断共享引用链
不能靠“约定不改”,得从初始化机制上杜绝共享。最直接的办法是把可变数据移进 __init__,确保每次实例化都新建对象。
- ✅ 正确:
class SafeList: def __init__(self): self.items = [] # 每个实例独享一份空列表 - ✅ 可选(延迟初始化):
class LazyList: items = None def __init__(self): if self.items is None: self.items = [](注意:这仍不线程安全,仅适用于单线程简单场景) - ❌ 错误:
class Bad: items = []—— 所有隐患都源于这一行
真正容易被忽略的是:哪怕你只在一个地方用到了 append,只要类属性是可变对象,整个类的所有实例就已处于风险之中。检测时别只看“有没有显式修改”,重点扫一遍类体里所有 = []、= {}、= set() 的赋值。


















