码农高天
Python通过引入具备局部优先、单调性与扩展前序图约束的C3算法,在多继承拓扑产生声明冲突时直接抛出TypeError中断运行,以保障方法调用链的确定性。
AI-assisted summary of 【python】你知道MRO是什么嘛?你知道多继承的顺序是怎么决定的嘛?最硬核的MRO教程! — original source. Check the original for full context.
Source published:
Key ideas
- Python 采用 MRO(方法解析顺序)将类及其所有父类线性化为优先级队列,并在多继承方法调用时沿该队列线性前向查找,首次命中即执行。 (0:00)
- 自 Python 2.3 起,官方引入 Dylan 语言的 C3 算法作为 MRO 的核心解析算法,以确保局部优先顺序、单调性与扩展前序图这三大约束的一致性。 (5:00)
- 当不同父类的局部声明顺序发生互斥死锁(如分别声明 A 先于 B 与 B 先于 A)时,Python 会在类创建时直接抛出 TypeError 报错并阻止程序运行。 (10:00)
- 日常开发中排查多继承调用疑难无需手动推导复杂算法,可直接使用 Class.__mro__ 或 Class.mro() 查看最终确立的线性解析链。 (0:00)
Detailed notes
## Background 该视频由 YouTube 频道“码农高天”于 2023 年 10 月 22 日发布。视频简介指出,本期内容将硬核介绍 Python 中的方法解析顺序(Method Resolution Order,简称 MRO),即当一个类存在复杂的继承关系时,解释器究竟如何决定优先调用哪个父类的方法。
## TL;DR 视频从 Python 菱形继承中反直觉的方法调用现象切入,系统阐述了 MRO 的概念以及通过 `__mro__` 和 `.mro()` 获取解析链条的方法。随后,作者深入剖析了自 Python 2.3 沿用至今的 C3 算法底层机制,详解其保证一致性的三大核心属性:局部优先顺序、单调性与扩展前序图。最后,视频通过具体的代码冲突实例展示了当继承顺序相互矛盾时解释器如何抛出 TypeError 崩溃,并针对日常开发中多继承的使用与排查给出了务实建议。
## Atmosphere & style 整体氛围极具硬核极客气质与学术推演感,节奏紧凑且逻辑极其严密。主讲人通过代码实机演示与论文溯源相结合的方式,层层剥开反直觉现象背后的底层数学与拓扑逻辑。观众在观看时既能感受到拨开底层黑盒机制的智力快感,又能在面对冲突死锁与工程权衡时体会到务实笃定的编码哲学。
## Bulletin - [00:00](https://www.youtube.com/watch?v=wbVP20WlMbc&t=0s) Python 采用 MRO(方法解析顺序)将类及其所有父类线性化为优先级队列,并在多继承方法调用时沿该队列线性前向查找,首次命中即执行。 - [05:00](https://www.youtube.com/watch?v=wbVP20WlMbc&t=300s) 自 Python 2.3 起,官方引入 Dylan 语言的 C3 算法作为 MRO 的核心解析算法,以确保局部优先顺序、单调性与扩展前序图这三大约束的一致性。 - [10:00](https://www.youtube.com/watch?v=wbVP20WlMbc&t=600s) 当不同父类的局部声明顺序发生互斥死锁(如分别声明 A 先于 B 与 B 先于 A)时,Python 会在类创建时直接抛出 TypeError 报错并阻止程序运行。 - [00:00](https://www.youtube.com/watch?v=wbVP20WlMbc&t=0s) 日常开发中排查多继承调用疑难无需手动推导复杂算法,可直接使用 Class.__mro__ 或 Class.mro() 查看最终确立的线性解析链。
## Key takeaways - [00:00](https://www.youtube.com/watch?v=wbVP20WlMbc&t=0s) 在面向对象编程中,简单的直觉规则(如先声明先匹配)在菱形多继承场景下往往失效,需要系统性的算法来判定优先级。 - [00:00](https://www.youtube.com/watch?v=wbVP20WlMbc&t=0s) MRO(Method Resolution Order)是 Python 决定从哪个父类优先查找并执行方法的线性化队列,类自身始终拥有最高优先级。 - [00:00](https://www.youtube.com/watch?v=wbVP20WlMbc&t=0s) 在 Python 中可通过 Class.__mro__(返回元组)或 Class.mro()(返回列表)获取当前类确立的 MRO 顺序。 - [05:00](https://www.youtube.com/watch?v=wbVP20WlMbc&t=300s) Python 2.3 开始采用源于 1996 年 Dylan 语言的 C3 线性化算法,彻底解决了传统算法在继承拓扑上的不一致缺陷。 - [05:00](https://www.youtube.com/watch?v=wbVP20WlMbc&t=300s) C3 算法的核心优势在于同时保证了局部优先顺序(Local Precedence Order)、单调性(Monotonicity)和扩展前序图(Extended Precedence Graph)。 - [05:00](https://www.youtube.com/watch?v=wbVP20WlMbc&t=300s) 局部优先顺序要求类声明中书写靠前的父类必须排在靠后的父类之前,且该先后顺序会递归约束其所有后代派生类。 - [07:22](https://www.youtube.com/watch?v=wbVP20WlMbc&t=442s) 单调性要求派生类解析方法时必须沿用直接父类确定的决策链,禁止越权推翻直接父类而向上层祖先类优先解析。 - [10:00](https://www.youtube.com/watch?v=wbVP20WlMbc&t=600s) 扩展前序图通过两类的“最小公共子类”建立偏序约束,确保体系内混入类(Mixin)与基类的相对优先级在全局保持一致。 - [10:00](https://www.youtube.com/watch?v=wbVP20WlMbc&t=600s) 若派生类的多个父类存在互相矛盾的局部先后声明,C3 算法将无法构建拓扑序列,Python 解释器会抛出 TypeError 崩溃中断。 - [10:00](https://www.youtube.com/watch?v=wbVP20WlMbc&t=600s) 尽管多继承在编程语言设计中存在争议,但在 Python 工程中,合理结合 .mro() 检查工具并保持继承层次扁平是务实的实践方案。
## Detailed walkthrough ### [00:00](https://www.youtube.com/watch?v=wbVP20WlMbc&t=0s) 为什么需要理解方法解析顺序(MRO)
在探讨 Python 底层实现时,MRO(Method Resolution Order,方法解析顺序)是一个无法绕开的核心概念。在面向对象编程中,一个类可以继承其他类的方法或属性。在单继承的简单场景下,逻辑十分直观:如果类 `M` 继承自类 `A`,当实例化 `M` 并调用方法时,程序会自然去执行父类 `A` 中的实现。然而,一旦继承结构变得复杂,直觉往往会失效。例如让类 `B` 继承类 `A`,再让类 `M` 继承类 `B`,此时调用父类方法会输出 `B` 的结果;如果改为类 `A` 与类 `B` 相互独立,而类 `M` 同时继承 `(A, B)`,根据先声明先匹配的直觉,输出会是 `A`。
真正的困惑出现在更复杂的多继承结构中。若新增一个继承自 `A` 的类 `C`,并让 `M` 继承 `(C, B)`,调用同名方法时输出仍是 `A`;但仅仅在代码中让 `B` 也继承 `A`(即典型的菱形继承),其余完全不变,输出结果却会出人意料地变成 `B`。面对这种看似反直觉的输出,码农高天指出:“在 Python 里面一个 class 如何去从它的父类里面找应该优先使用哪个父类的函数,这个顺序就叫 MRO,也就是 Method Resolution Order。”每一个类都会将自身及其所有父类进行线性化(serialization),排成一个具有严格优先级的队列,类自身拥有最高优先级。当需要调用方法或访问属性时,解释器会严格按照该优先级队列从前向后查找。
```mermaid flowchart TD accTitle: Python多继承菱形关系示意图 accDescr: 展示类M同时继承C与B,而C与B又共同继承A的多继承结构
subgraph Base["顶层基类"] A["类 A"] end subgraph Middle["中间父类"] C["类 C"] --> A B["类 B"] --> A end subgraph Derived["派生类"] M["类 M"] --> C M --> B end ```
在 Python 中获取类的 MRO 线性化顺序有两种标准方式:直接访问类的属性 `类名.__mro__`,或者调用内置方法 `类名.mro()`。通过打印可以发现,上述菱形继承中 `M` 的 MRO 实际上是 `[M, C, B, A, object]`,这精确解释了为何最终匹配到的是 `B` 而不是 `A`,因为 `B` 的优先级排在 `A` 之前;而在 `B` 未继承 `A` 的旧版本中,其 MRO 则是 `[M, C, A, B, object]`,因此优先匹配到 `A`。
- 获取 MRO 的两种方式:`Class.__mro__` 返回元组;`Class.mro()` 返回列表。 - 继承查找机制:构建新类时首先根据继承关系推导出 MRO 线性序列,方法调用时沿着序列线性前向探测,首次命中即执行。
---
[05:00](https://www.youtube.com/watch?v=wbVP20WlMbc&t=300s) C3 线性化算法与局部优先顺序(Local Precedence Order)
很多入门教程讲完 MRO 的概念与获取方式就戛然而止,但这并未触及底层的算法逻辑。自 Python 2.3 起,Python 官方正式引入了 1996 年在 Dylan 语言中首次提出的 C3 算法作为 MRO 的核心解析算法。C3 算法之所以冠以“C3”之名,是因为它能够在三个核心属性上做到一致性(Consistent)保证:扩展前序图(Extended Precedence Graph)、局部优先顺序(Local Precedence Order)以及单调性(Monotonicity)。
在这三个性质中,相对最容易理解的是局部优先顺序(Local Precedence Order)。它的基本定义是:当一个派生类显式继承多个父类时,该类必须优先使用在类定义中书写在前面的父类的方法。例如定义 `class M(A, B): pass` 时,由于 `A` 声明在 `B` 之前,局部优先顺序严格保证优先使用 `A` 的方法;若调换声明顺序为 `class M(B, A): pass`,则必定优先使用 `B` 的方法。
C3 算法对局部优先顺序的要求更为严苛,它不仅约束当前类,还递归约束其所有后代。码农高天强调:“只要你继承了 M,那么你的 MRO 里面 B 就必须要在 A 前面,这个是 local precedence order。”换言之,任何继承了 `M` 的子类,其解析序列都不得破坏 `M` 在继承声明中所确立的局部先后关系。
- C3 算法的历史:1996 年提出,最初应用于 Dylan 语言,Python 从 2.3 版本开始将其作为标准的 MRO 计算算法。 - C3 的三个核心一致性属性:局部优先顺序(Local Precedence Order)、单调性(Monotonicity)、扩展前序图(Extended Precedence Graph)。
---
[07:22](https://www.youtube.com/watch?v=wbVP20WlMbc&t=442s) 保持继承链一致的单调性(Monotonicity)
单调性(Monotonicity)是多继承算法中较难直观理解的第二个核心性质。单调性本质上要求子类必须继承父类的方法决策,而不能越权颠覆直接父类的解析结果。具体而言,任何一个类所解析使用的方法,必须来自其直接父类(Direct Superclass)所采用的方法解析链条,不能越过直接父类擅自向更高阶的祖先类回溯。
在复杂的网状多继承中,若类 `D` 继承自 `(C, B)`,且最终确立使用的是父类 `A` 中的方法实现,那么直接继承 `D` 的派生类 `M` 在解析同名方法时,也必须沿用 `A` 的实现。如果一个算法计算出的 MRO 导致 `M` 在解析方法时跳过了 `D` 所确立的匹配结果,而优先拾取了另一个更高层级父类的实现,这便构成了对单调性的破坏。
在 C3 论文中给出的经典反例生动说明了单调性被破坏的后果: 在某种缺陷线性化算法中,定义了小艇(Day Boat)与轮船(Wheel Boat)等混合继承体系。派生类明轮船(Paddle Wheel Boat)的解析顺序中,小艇排在轮船前面;而小型双体船(Small Catamaran)仅继承小艇;但在同时继承了两者的脚踏船(Pedalo)中,算法却将轮船排在了小艇前面。此时,如果某个方法仅存在于小艇与轮船中,脚踏船的两个直接父类本都应该解析到小艇的方法,但脚踏船自身最终调用的却是轮船的方法。这种子类推翻父类决策的现象,即违背了单调性。
---
[10:00](https://www.youtube.com/watch?v=wbVP20WlMbc&t=600s) 扩展前序图(Extended Precedence Graph)与 MRO 的有效性冲突
C3 算法中最难理解的第三个属性是扩展前序图(Extended Precedence Graph,简称 EPG)。继承关系中的前序图定义了子类与父类之间的有向依赖,而扩展前序图则进一步为图中的任意两个类确立偏序关系。确立规则取决于这两个类的“最小公共子类”(Lowest Common Subclass,即该类是两者的共同子类,且该类的任何直接父类都不是两者的共同子类)。在最小公共子类中,只要类自身或其某个派生类排在另一个类(或其派生类)之前,这个前后关系就必须固化到整个系统的优先级判定中。
论文中以带滚动功能与编辑功能的面板为例进行了说明:对于 `ScrollingMixin` 与 `EditingMixin` 这两个混入类,它们的最小公共子类是同时具备滚动和编辑功能的 `EditableScrollablePane`。在该子类中,由于书写声明时的局部优先顺序,滚动面板(`ScrollablePane`)排在编辑面板(`EditablePane`)之前,而它们分别派生自对应的混入类。因此,依据扩展前序图的约束,在整个解析拓扑中,`ScrollingMixin` 的全局优先级必须先于 `EditingMixin`。
然而,并不是任意设计的多继承结构都能在满足上述三大约束的前提下计算出合法的 MRO。码农高天在代码中构造了明显的冲突案例: 若类 `C` 继承自 `(A, B)`,要求 `A` 必须排在 `B` 之前;而类 `D` 继承自 `(B, A)`,要求 `B` 必须排在 `A` 之前。此时若定义一个新类 `M` 同时继承 `(C, D)`,系统便陷入了无法解开的死锁。VS Code 等现代编辑器能直接识别语法层面的矛盾,在实际运行代码时,Python 解释器更会在编译构建类对象阶段直接抛出 `TypeError`,明确报错无法构建一致的方法解析顺序(`Cannot create a consistent method resolution order`)。
```mermaid flowchart TD accTitle: MRO冲突死锁示意图 accDescr: 展示类C要求A先于B,而类D要求B先于A,导致子类M产生继承冲突
subgraph ConflictClasses["声明顺序相反的父类"] C["class C(A, B)<br/>要求: A > B"] D["class D(B, A)<br/>要求: B > A"] end subgraph Target["目标派生类"] M["class M(C, D)"] --> C M --> D end Target -.->|"抛出 TypeError<br/>无法满足一致性"| Err["崩溃:Cannot create a consistent MRO"] ```
关于面向对象中多继承的存在意义,程序设计领域长期存在激烈争论。部分程序员持极端立场,认为多继承甚至继承本身是设计的“原罪”,因此某些现代编程语言完全摒弃了多继承,仅支持单继承或接口组合。但在 Python 等主流动态语言中,多继承仍被视为合法且强大的特性。面对复杂的算法规则,码农高天对日常开发给出了务实的建议:“你可以不用管过程,你可以不用管原理,起码你要知道怎么拿到结果。”在实际编码中,尽量保持继承关系的简洁明晰,在遇到疑难排查时善用 `.mro()` 检查实际解析队列,便足以应对绝大部分工程场景。
- 语法崩溃原因:当父类之间存在相互矛盾的局部先后声明时,C3 算法无法完成拓扑排序,直接抛出 `TypeError` 中断执行。 - 架构设计权衡:多继承极易滋生菱形继承与命名冲突,实际工程中应优先考虑扁平结构,或直接使用属性查询验证优先级。
## Notable quotes > [00:00](https://www.youtube.com/watch?v=wbVP20WlMbc&t=0s) 在 Python 里面一个 class 如何去从它的父类里面找应该优先使用哪个父类的函数,这个顺序就叫 MRO,也就是 Method Resolution Order。 — 码农高天
> [10:00](https://www.youtube.com/watch?v=wbVP20WlMbc&t=600s) 你可以不用管过程,你可以不用管原理,起码你要知道怎么拿到结果。 — 码农高天
## Mentioned Python, MRO, C3 线性化算法, Dylan, VS Code, TypeError
## Video description 这期视频介绍一下MRO,Method Resolution Order,也就是当一个类有多个继承关系的时候,是如何决定使用哪个类的方法的。内容偏硬核,请准备好大脑。
[Source](https://www.youtube.com/watch?v=wbVP20WlMbc)