类与属性查找
一、a.interest = 0.05 到底改了什么
- 这一篇对应官方 Lecture 19(Classes)与 Lecture 20(Attributes),换成类来组织数据与行为。
假设 Account 类上有 interest = 0.02,另有一个实例 a。执行:
1 | a.interest = 0.05 |
「改利率」听起来理所当然。但 Account.interest 仍是 0.02,其他实例也仍是 0.02。那个 0.05 究竟被存到了哪里?
答案是:它新建了一个实例属性,把类属性挡住了。这一招叫遮蔽(shadowing),是这一篇的核心陷阱。
二、类、实例与 self
1 | class Account: |
输出:
1 | 20 |
四行输出,讲了两件事:
Account('Jo')触发实例化,自动调用__init__,self就是新建的这个实例。a.deposit(20)与Account.deposit(a, 20)完全等价——a.deposit是一个绑定方法:函数本身 + 已经绑好的self。所以你写a.deposit(20)时,self是被自动补上的。self.balance += amount里的self.不能省。
第二条值得单独看:
1 | class Account: |
输出:
1 | broken → 0 |
balance = amount 只是在帧里建了个局部变量,跟 self.balance 毫无关系,方法一返回就随帧消失。不报错,但什么也没做——这是 OOP 里最难自查的一类 bug,因为程序不会报错,只是静默失效。
还有一处对称的问题:定义方法时漏写 self 形参。
1 | class Bad: |
输出:
1 | 调用报错 → TypeError |
b.deposit(10) 实际会调成 Bad.deposit(b, 10)——两个实参,形参只收一个,于是 TypeError。掌握这条就能反推:报「多传了一个参数」,先怀疑漏写 self。
三、实例属性 vs 类属性
两者的区别只有一条,但后果很大:
| 属性写在哪 | 存在哪 | 谁共享 |
|---|---|---|
__init__ 里 self.x = ... |
实例上 | 只有这个实例 |
类体里 x = ... |
类上 | 所有实例共享(除非被遮蔽) |
1 | class Account: |
输出:
1 | 0.02 0.02 0.02 |
最后一行是小整数驻留造成的 True,别当语义——真正要注意的是类属性是共享的,实例属性是各自的。
四、遮蔽:最容易出错的一步
1 | class Account: |
输出:
1 | 0.02 0.02 |
最后一行是铁证:a 的 __dict__ 里多了一个 interest,而 b 里没有。a.interest = 0.04 并没有「改类的利率」,它只是在实例上新建了一个同名属性,从此查找时先命中它,类属性被挡住。
点表达式的查找顺序是:
flowchart LR
S["a.interest"] --> Q1{"实例 a 上有<br/>interest 吗?"}
Q1 -->|"有"| R1["返回实例属性"]
Q1 -->|"没有"| Q2{"类 Account 上有吗?"}
Q2 -->|"有"| R2["返回类属性"]
Q2 -->|"没有"| Q3{"基类上有吗?<br/>(后续讲继承)"}
Q3 -->|"有"| R3["返回基类属性"]
Q3 -->|"都没有"| E["AttributeError"]
先在实例上找,找不到再往上找,第一个命中就停。 这是本讲最重要的一句话;继承所做的,只是把这条链再延长一格。
想改所有实例共享的那个值,必须通过类名:
1 | class Account: |
输出:
1 | 0.02 |
五、复合赋值的两处隐患
a.interest += 0.01 看起来只是「把利率加一点」。跑一遍:
1 | class Account: |
输出:
1 | 0.02 0.02 |
+= 是「先读、再算、再写回」,而「写回」是对 a.interest 赋值——根据第三节的规则,这就等于在实例上创建了一个属性。于是类属性又一次被遮蔽,而且这次是「不小心」发生的。
于是 a 从此跟别的实例分道扬镳:改 Account.interest 也影响不到它了。
这种写法极易造成误判。 看到 实例.类属性 += ... 时,先判断:它是要改类属性,还是要改这个实例?
六、小结
| 概念 | 一句话 | 证据 |
|---|---|---|
a.f(x) ≡ A.f(a, x) |
self 自动补 |
两者输出都是 40 |
漏写 self |
调用时多传一个实参 | TypeError |
漏写 self. |
只建了局部变量,静默失效 | broken → 0 |
| 查找顺序 | 实例 → 类 → 基类,首个命中即停 | a.__dict__ 里有 interest |
| 类属性共享 | 所有实例共用一份 | Account.interest 一改全变 |
| 遮蔽 | a.x = v 新建实例属性挡住类属性 |
a.interest 变、Account.interest 不变 |
| 复合赋值 | a.x += v 会意外创建实例属性 |
a.interest → 0.03 |
三条能带走的:
a.f(x)就是A.f(a, x)。 报「多传了一个参数」就查self;报「属性不存在」就查self.是不是漏了。- 点查找:实例优先,逐级向上回退,首个命中即停。 这是继承、
super()、__mro__的共同基础。 - 通过实例赋值只会创建实例属性,永远不会改到类属性。 想改共享值,必须写
类名.属性 = ...。

