class Animal: def speak(self): print("动物叫") class Dog(Animal): pass d = Dog() d.move() # 假设写成 d.speak(),输出:动物叫
Dog 自己没写 speak,但 d.speak() 却能打印"动物叫"。
*问题来了*:Python 怎么知道去 Animal 里找的?
AI整理
MRO:一张固定不变的"顺序表"
*MRO*(Method Resolution Order,方法解析顺序)就是 Python 为每个类准备的一张顺序表,写明了"找方法时,按什么顺序翻"。
class Animal: def speak(self): print("动物叫") class Dog(Animal): pass print(Dog.__mro__) # (<class '__main__.Dog'>, <class '__main__.Animal'>, <class 'object'>)
顺序是:Dog → Animal → object。
d.speak() 发生时,Python 就按这张表挨个翻:
| *步骤* | *去哪翻* | *有没有****\*speak\**** | *动作* |
|---|---|---|---|
| ① | Dog | 没有(pass) | 继续 |
| ② | Animal | *有!* | 调用它,打印"动物叫",停 |
*核心认知*:MRO 是"先翻谁、后翻谁"的规则。不管那个类里有没有这个方法,它都老老实实排在表里;没有就跳过,找到了就停。
括号里的顺序决定谁在前
class Dog(Flyable, Swimmable) 里谁写前面,谁就排在 MRO 前面。
class Flyable: def move(self): print("飞") class Swimmable: def move(self): print("游") class Duck(Flyable, Swimmable): pass print(Duck.__mro__) # Duck → Flyable → Swimmable → object Duck().move() # 输出:飞 ← Flyable 排在前面,先被找到
把顺序换一下:
class Duck(Swimmable, Flyable): pass print(Duck.__mro__) # Duck → Swimmable → Flyable → object Duck().move() # 输出:游 ← Swimmable 排在前面了
*一句话*:括号里靠前的父类优先级高,它的方法会把后面的同名方法盖住。
父类没有这个方法?跳过,继续找
上一节两个父类都写了 move,那如果*只有一个有*呢?
class AGOOD: pass class Animal: def speak(self): print("动物叫") class Dog(AGOOD, Animal): # AGOOD 在前,但它没有 speak pass Dog().speak() # 输出:动物叫 print(Dog.__mro__) # Dog → AGOOD → Animal → object
查找过程:
| *步骤* | *去哪翻* | *有没有****\*speak\**** | *动作* |
|---|---|---|---|
| ① | Dog | 没有 | 继续 |
| ② | AGOOD | 没有 | 继续 |
| ③ | Animal | *有!* | 调用,打印"动物叫" |
AGOOD 里没有 speak,*只是被"翻了一下"就跳过了,链条不会断,也不会报错。*
父类里有另一个方法,不受影响
class Flyable: def fly(self): print("飞") class Swimmable: def move(self): print("游") class Dog(Flyable, Swimmable): pass d = Dog() d.fly() # 输出:飞 ← 在 Flyable 找到 fly d.move() # 输出:游 ← 在 Swimmable 找到 move
*每次调用找的是那个具体的方法名。*Flyable 靠前,所以 fly 一定在它那里找到;查 move 时它缺席,就轮到 Swimmable。
*一句话*:父类里没有这个方法就跳过,继续查下一个。有没有方法,跟排在第几位无关。
同名方法:谁排在前面谁赢
把第 3 节和第 4 节合起来看,就是同一个机制的两种表现:
| *情况* | *结果* | *为什么* |
|---|---|---|
| 两个父类*都有*同名方法 | 排在前面的那个赢,后面的被盖住 | 找到就停 |
| 只有一个父类有这个方法 | 照常找到它,前面的"空"父类被跳过 | 没有就跳过 |
想让两个都执行?得用 super()
如果真想让"飞"和"游"都打印,不能只靠 MRO,得*主动把链条接下去*
class Flyable: def move(self): print("飞") super().move() # 接下去,找下一个 class Swimmable: def move(self): print("游") class Duck(Flyable, Swimmable): def move(self): print("鸭子上场") super().move()
输出:
鸭子上场 飞 游
*注意*:如果不写 super(),就是"找到我就结束";写了 super(),才是"我先干完,再交给下一个"。
继承关系决定 MRO 的顺序
前面几节,父类的继承关系都很简单——大家都只继承 object。*一旦父类之间也有继承关系,MRO 的顺序就不只是"括号顺序"那么简单了。*
6.1 关键对照:去掉 (SWWW) 会怎样
这是理解 MRO 最重要的一组对照。*只改****\*Swimmable\*****后面有没有****\*(SWWW)\****,其他一律不动:
# ===== 版本 A:Swimmable() 不继承 SWWW ===== class SWWW: def move(self): print("SWWW 移动") class Swimmable: # ← 没有 (SWWW) def move(self): print("游") super().move() class Flyable(SWWW): def move(self): print("飞") super().move() class Dog(Flyable, Swimmable): pass print([c.__name__ for c in Dog.__mro__]) # ['Dog', 'Flyable', 'SWWW', 'Swimmable', 'object'] ← SWWW 在 Swimmable 前面! Dog().move() # 飞 # SWWW 移动 ← "游"没打印,Swimmable 被跳过了 # ===== 版本 B:Swimmable(SWWW) 继承 SWWW ===== class SWWW: def move(self): print("SWWW 移动") class Swimmable(SWWW): # ← 有 (SWWW) def move(self): print("游") super().move() class Flyable(SWWW): def move(self): print("飞") super().move() class Dog(Flyable, Swimmable): pass print([c.__name__ for c in Dog.__mro__]) # ['Dog', 'Flyable', 'Swimmable', 'SWWW', 'object'] ← Swimmable 回到 SWWW 前面 Dog().move() # 飞 # 游 # SWWW 移动 ← "游"打印了
*对比表*:
*版本 A:****\*Swimmable()\**** | *版本 B:****\*Swimmable(SWWW)\**** | |
|---|---|---|
| MRO 后半段 | Flyable → **SWWW → Swimmable** | Flyable → **Swimmable → SWWW** |
d.move() 输出 | 飞 → SWWW 移动 | 飞 → 游 → SWWW 移动 |
Swimmable.move 执行了吗 | ❌ 被 SWWW 盖住 | ✅ |
Swimmable 里的 super() | 死代码,没跑到 | 执行了,调到 SWWW |
*为什么会反过来?* 只因为一条规则:*子类必须排在父类前面。*
版本 A:
Swimmable和SWWW谁都不认识,没这条约束,SWWW紧跟Flyable排前面,Swimmable被挤到后面。版本 B:
Swimmable成了SWWW的子类,必须排在SWWW前面,所以回到SWWW之前。
*陷阱*:版本 A 里 Swimmable 里的 super().move() 是一行*死代码*——因为 Swimmable.move 根本没被调用,里面的代码自然一行都不会执行。你以为写了 super() 就一定会往下走,其实前提是"这个类的这个方法得先被找到"。
6.2 那括号顺序还管用吗?
管用,但它*要服从"子类在父类前面"这条硬约束*。
class Dog(Swimmable, Flyable): # 把 Swimmable 写前面 pass
版本 B 下,MRO 变成 Dog → Swimmable → Flyable → SWWW → object,输出变成 游 → 飞 → SWWW 移动——Swimmable 靠括号顺序排到最前。
*一句话*:继承关系决定"谁必须在谁前面",括号顺序在剩下的空间里决定"谁更靠前"。Python 把两者合起来,算出唯一合法的一张表。
super() 是一次新的查找
很多人以为 super() 就是"调用父类",这是最大的误解。
7.1 "找到就停"和"super() 接力"并不矛盾
class SWWW: def move(self): print("SWWW 移动") class Swimmable(SWWW): def move(self): print("游") super().move() class Flyable(SWWW): def move(self): print("飞") super().move() class Dog(Flyable, Swimmable): def move(self): print("狗上场") super().move()
输出:
狗上场 飞 游 SWWW 移动
*为什么会全部走一遍?* 因为这里一共发生了 *4 次查找*,每次都遵守"找到就停":
| *第几次* | *谁发起* | *从哪开始找* | *停在哪* | *打印* |
|---|---|---|---|---|
| 第 1 次 | d.move() | Dog | Dog.move | 狗上场 |
| 第 2 次 | Dog 里的 super() | Flyable | Flyable.move | 飞 |
| 第 3 次 | Flyable 里的 super() | Swimmable | Swimmable.move | 游 |
| 第 4 次 | Swimmable 里的 super() | SWWW | SWWW.move | SWWW 移动 |
*"找到就停"*:说的是*每一次查找*。第 1 次在
Dog就停了,第 2 次在Flyable停了……*"全部走一遍"*:是因为每次停下的地方,自己又用
super()*发起了下一次查找*。
打个比方:*不是一个人沿楼梯往下走,而是每一层的人干完活,按一下电梯按钮,让电梯把活送到下一层。* 每层都停,只是每层都会再叫一次电梯。
7.2 去掉一个 super(),链条立刻断
class Flyable(SWWW): def move(self): print("飞") # 没写 super()
输出:
狗上场 飞
第 2 次查找照样在 Flyable 停下,但这次它没写 super(),*不再发起第 3 次*。Swimmable 和 SWWW 一次都没被翻到。
*谁不写****\*super()\*****,链条就断在谁那里。* 这和是不是菱形继承、是不是多继承无关。
7.3 super() 找的是"方法名",不是"父类"
这一点最绕,看个例子:
class SWWW: def move(self): print("SWWW 移动") class Swimmable(SWWW): def move(self): print("游") class Flyable(SWWW): def fly(self): # ← 只有 fly,没有 move print("飞") super().move() # ← super() 写在 fly 里,找的是 move class Dog(Flyable, Swimmable): pass d = Dog() d.fly() # 飞 # 游
执行过程:
| *步骤* | *做什么* | *说明* |
|---|---|---|
| ① | 找 fly,在 Flyable 找到 | 打印"飞" |
| ② | fly 里的 super().move() | *新的一次查找,找的是****\*move\**** |
| ③ | 从 Flyable 后面找 move | 在 Swimmable 找到,打印"游" |
***\*super()\*****只认 MRO 里"下一个类"和"那个方法名"。* 所以这里调到的是 Swimmable.move,而不是" Flyable 的父类 SWWW 的 move"。SWWW 的 move 排在 Swimmable 后面,轮不到它。
*一句话*:super() 不是"调父类",而是"按 MRO 从下一个类开始,找那个同名方法"。
7.4 Dog 自己不写 move 会怎样
把第 7.1 节的 Dog 改成 pass:
class Dog(Flyable, Swimmable): pass Dog().move() # 飞 # 游 # SWWW 移动
***\*Dog\*****自己有****\*move\**** | ***\*Dog\*****自己没****\*move\**** | |
|---|---|---|
| 第 1 次查找停在哪 | Dog.move | Flyable.move |
| 输出 | 狗上场 → 飞 → 游 → SWWW 移动 | 飞 → 游 → SWWW 移动 |
| 一共几次查找 | 4 次 | 3 次 |
*只差在"第一次停在哪儿"。* 后面的接力完全一样,因为 super() 和 MRO 没变。
菱形继承:老祖宗被压到最底下
第 6 节的结构其实就是*菱形继承*(也叫钻石继承):
SWWW / \ Flyable Swimmable \ / Dog
两条线汇到同一个老祖宗 SWWW 上。这是 MRO 算法真正要解决的核心难题。
8.1 没有 MRO 规则会出什么问题
如果按"深度优先"的老算法(Python 2 的经典类),查 Dog.move 的路径是:
Dog → Flyable → SWWW(找到!) → 停 ← Swimmable 被跳过了!***\*Swimmable\*****被彻底无视*,它重写的 move 永远没机会执行。
8.2 MRO 如何解决这个问题
Python 的 MRO 算法(C3 算法)保证了一件事:
*每条继承线上的重写,都一定比老祖宗优先被查到。*
所以第 6 节的表里,Swimmable 和 Flyable 都排在 SWWW 前面,不会出现中间层被跳过的情况。
8.3 菱形继承里 super() 的真正价值:每个类只执行一次
class SWWW: def move(self): print("SWWW 移动") class Swimmable(SWWW): def move(self): print("游") super().move() class Flyable(SWWW): def move(self): print("飞") super().move() class Dog(Flyable, Swimmable): def move(self): print("狗上场") super().move()
MRO:Dog → Flyable → Swimmable → SWWW → object
输出:狗上场 → 飞 → 游 → SWWW 移动。
***\*SWWW.move\*****只执行了一次*,虽然理论上它可以通过 Flyable 和 Swimmable 两条路被调到。*因为 MRO 把****\*SWWW\*****合并成一个位置*,链条只经过它一次。
这正是 super() 在菱形继承里不可替代的原因——用硬编码的 SWWW.move(self) 反而可能重复调用,而 super() 保证不重不漏。
8.4 对照:各种写法的结果
| *写法* | *输出* | *说明* |
|---|---|---|
全部用 super()(推荐) | 狗上场 → 飞 → 游 → SWWW 移动 | 每个类一次,不重不漏 |
Swimmable 里硬编码 SWWW.move(self) | 同上,但链条断了 | 以后 SWWW 被替换会调错人 |
Swimmable 忘了写 super() | 狗上场 → 飞 → 游 | SWWW 丢了 |
Dog(Swimmable, Flyable) 且全用 super() | 游 → 飞 → SWWW 移动 | 顺序变了,但都执行 |
8.5 菱形继承会报错的情况
class Dog(Flyable, Swimmable, Flyable): # 同一个类写两次 pass TypeError: Cannot create a consistent method resolution order (MRO) for bases Flyable, Swimmable
因为 Flyable 要在 SWWW 前面(自身继承),又要在 Swimmable 后面(第二次出现的位置),互相矛盾,排不出来。
这条报错是好事——MRO 算法在类定义时就帮你检查了继承结构是否合法。
总结:三句话 + 一张图
三句话
*MRO 是一张顺序表*,在
class定义时算好,永远不变;调用和方法里有没有super()都改不了它。*找方法就是按表挨个翻*:有这个方法就调用并停下,没有就跳过看下一个。
***\*super()\*****是一次新的查找*,从表里"下一个类"开始,找那个同名方法——它只认表和名字,不认"父类"。
一张图
MRO = 火车编组(印好就不变): [Dog] ──► [Flyable] ──► [Swimmable] ──► [SWWW] ──► [object] super() = 车门: Dog.move ──开门──► Flyable.move ──开门──► Swimmable.move ──开门──► SWWW.move 每层都"找到就停",只是每层都会再开一次门。 没门(没写 super()),就停在这一层。
三个东西别混
| *概念* | *什么时候确定* | *会不会变* | *作用* |
|---|---|---|---|
| *MRO 顺序表* | class 定义那一行 | 永远不变 | 规定"先翻谁后翻谁" |
***\*super()\**** | 方法被调用时执行 | 每次按表往下走一格 | 发起下一次查找 |
| *方法有没有被找到* | 查找过程中 | 找到就停 |
