码农高天
super并非查找直接父类的语法糖,其本质是接收类与实例的双参数对象,执行逻辑由传入对象的动态MRO链决定,多继承下会直接调用平级兄弟类。
AI-assisted summary of 【python】B站最细致的super()详解,一定有你不知道的知识! — original source. Check the original for full context.
Source published:
Key ideas
- super 在 Python 中并非函数、方法或关键字,而是一个正儿八经的内置类(在 CPython 中对应 PySuper_Type),每次调用 super(...) 实际上是实例化了一个 super 对象。 (0:00)
- super 的完整原型接收两个参数:第一个参数(类)决定从 MRO 链的哪一个位置向后检索,第二个参数(对象或类)决定以谁的 MRO 为基准并将方法绑定到该实体上。 (0:00)
- 在显式传递两个参数的前提下,super 完全可以在类定义外部任意使用,例如在未初始化实例的外部直接调用父类的 __init__ 方法。 (5:00)
- 在复杂多继承场景中,super() 不一定会调用该类声明的直属父类,而是严格按照运行时传入实例的 MRO 链顺序查找,甚至可能直接调用其平级类的同名方法。 (10:00)
Detailed notes
## Background 该内容由知名技术博主“码农高天”于 2023 年 10 月 30 日发布在 YouTube 平台。视频简介指出,本期主题应观众要求深入讲解 Python 中的 `super()` 机制,并结合先前关于 MRO(方法解析顺序)的内容展开进阶剖析。
## TL;DR 视频首先纠正了关于 `super` 是函数或关键字的常见误区,指出其本质是 CPython 底层的内置类 `PySuper_Type`。随后,作者详细拆解了 `super(type, object_or_type)` 的双参数运行原理,演示了如何通过第一个参数跳过特定类检索方法,并展示了在类定义外部直接调用 `super` 的黑魔法。最后,视频通过经典的多继承案例揭示了协和继承的动态调用陷阱,证明在特定 MRO 链下父类的 `super()` 可能会调用到看似无关的平级类方法。
## Atmosphere & style 视频呈现出硬核干货与轻松幽默兼备的技术极客风格,主讲人讲解严密、循序渐进,充满探究源码底层的求真热情。随着由浅入深的活体代码演练和反直觉报错的抛出,讲解节奏张弛有度,在“颠覆日常认知”与“原来如此”的顿悟感之间自如切换,极具思维启发性与观赏趣味。
## Bulletin - [00:00](https://www.youtube.com/watch?v=bfuBVu7YALk&t=0s) super 在 Python 中并非函数、方法或关键字,而是一个正儿八经的内置类(在 CPython 中对应 PySuper_Type),每次调用 super(...) 实际上是实例化了一个 super 对象。 - [00:00](https://www.youtube.com/watch?v=bfuBVu7YALk&t=0s) super 的完整原型接收两个参数:第一个参数(类)决定从 MRO 链的哪一个位置向后检索,第二个参数(对象或类)决定以谁的 MRO 为基准并将方法绑定到该实体上。 - [05:00](https://www.youtube.com/watch?v=bfuBVu7YALk&t=300s) 在显式传递两个参数的前提下,super 完全可以在类定义外部任意使用,例如在未初始化实例的外部直接调用父类的 __init__ 方法。 - [10:00](https://www.youtube.com/watch?v=bfuBVu7YALk&t=600s) 在复杂多继承场景中,super() 不一定会调用该类声明的直属父类,而是严格按照运行时传入实例的 MRO 链顺序查找,甚至可能直接调用其平级类的同名方法。
## Key takeaways - [00:00](https://www.youtube.com/watch?v=bfuBVu7YALk&t=0s) super 并不是内置函数或关键字,其实质是 Python 底层的内置类型(CPython 源码中为 PySuper_Type),调用 super(...) 本质上是构造一个 super 类的实例对象。 - [00:00](https://www.youtube.com/watch?v=bfuBVu7YALk&t=0s) 通常书写的无参形式 super() 只是语法糖,其完整形态为 super(type, object_or_type),接收两个必不可少的逻辑参数。 - [00:00](https://www.youtube.com/watch?v=bfuBVu7YALk&t=0s) super 的第一个参数(type)指定查找的起始参照点,其底层逻辑是跳过该类本身,从 MRO 链条中排在该类之后的下一个类开始检索。 - [00:00](https://www.youtube.com/watch?v=bfuBVu7YALk&t=0s) super 的第二个参数(object 或 type)提供用于检索的基准 MRO 链,同时在检索到目标函数后将其与该对象完成方法绑定(bind)。 - [00:00](https://www.youtube.com/watch?v=bfuBVu7YALk&t=0s) 相比硬编码父类名称(如 Person.__init__(self)),使用 super() 不仅便于后续重构与基类改名解耦,其核心价值在于支持动态调度(Dynamic Dispatch)。 - [05:00](https://www.youtube.com/watch?v=bfuBVu7YALk&t=300s) 通过手动调整 super 的第一个参数,可以精确控制向上检索的起点,甚至能够刻意跳过中间层的父类初始化逻辑,直接调用更上层的祖先类。 - [05:00](https://www.youtube.com/watch?v=bfuBVu7YALk&t=300s) super 并非必须在类体内使用;在模块全局或任意函数中,配合 Male.__new__ 生成的裸对象,直接执行 super(Male, m).__init__(...) 同样能合法执行并完成属性初始化。 - [05:00](https://www.youtube.com/watch?v=bfuBVu7YALk&t=300s) 无参数 super() 只能安全运行在类定义内部,Python 解释器在底层通过字节码等机制自动推断当前所处的类作用域和当前函数传入的第一个参数(self 或 cls)。 - [05:00](https://www.youtube.com/watch?v=bfuBVu7YALk&t=300s) 单参数形态 super(type) 会返回一个未绑定的 super 对象(unbound object),主要用于特殊元编程或配合描述符协议手动绑定。 - [10:00](https://www.youtube.com/watch?v=bfuBVu7YALk&t=600s) 在多继承场景下,由于方法解析严格依照传入实例(如子类 M)的完整 MRO 链,父类 B 中的 super() 可能会调用其平级类 C 中的方法,颠覆纯单继承的直觉。 - [10:00](https://www.youtube.com/watch?v=bfuBVu7YALk&t=600s) 这一机制在面向对象编程中被称为协同多继承(Cooperative Multiple Inheritance),要求开发者必须从实例整体的 MRO 拓扑顺序理解 super 的跳转逻辑,而非单纯视为找父类。
## Detailed walkthrough ### [00:00](https://www.youtube.com/watch?v=bfuBVu7YALk&t=0s) super() 的本质与完整形态
很多 Python 开发者对 `super` 的认知通常停留在最基础的用法:定义一个子类(例如 `Male`)继承自父类(例如 `Person`),然后在子类的 `__init__` 方法中通过 `super().__init__(...)` 来调用父类的初始化函数。对于许多人来说,这几乎就是关于 `super` 的全部知识。然而,关于 `super` 存在一个广泛流传的误区——不少人认为它是一个函数、方法,甚至有人误以为它是关键字。
码农高天纠正了这个冷知识:“实际上 super 是一个正儿八经的 class,它是一个内置类的名字,然后 super 括号并不是调用了一个函数,super 括号是建立了一个 super 的对象。”在 CPython 源码的 `typeobject.c` 文件中,它的定义被称为 `PySuper_Type`。它与大家平常频繁接触的 `list`、`dict` 完全一样,都是底层的 `type`,在 Python 语境下就是一个类。
```mermaid flowchart TD accTitle: super完整双参数工作机制 accDescr: 展示super接收类与对象参数后,如何依托MRO链定位方法并绑定执行
subgraph Parameters ["super(type, object_or_type)"] P1["第一个参数: type (Class)<br/>决定在 MRO 链中从哪个位置往后找"] P2["第二个参数: object / type<br/>决定提供 MRO 链,并绑定函数"] end
subgraph Process ["MRO 链检索与绑定流程"] direction TB MRO["获取 self 的 MRO 链:<br/>[Male, Person, Animal, object]"] Find["定位第一个参数 Male 的下一个节点:<br/>Person"] Lookup["在 Person 中查找目标函数 __init__"] Bind["将找到的方法 bind 到 self 上执行"] end
P2 -->|"提供 MRO 链"| MRO P1 -->|"跳过该类"| Find MRO --> Find Find --> Lookup Lookup --> Bind ```
通常大家习惯书写不带参数的 `super()`,但 `super` 的完整形态实际上接收两个参数:`super(type, object_or_type)`。 - **第一个参数(type)**:必须是一个类,它决定了在方法解析顺序(MRO)链条上从哪一个类**之后**开始往后查找。 - **第二个参数(type 或 object)**:可以是一个对象实例或类,它决定了两件事:第一,最终解析到的函数将绑定(bind)到哪一个对象或类上;第二,它决定了**以谁的 MRO 链**作为查找基准。
在 `Male` 类的初始化中,`super().__init__(name)` 的完整等价形式其实是 `super(Male, self).__init__(name)`。其背后的完整运作流程如下: 1. 从传入的第二个参数 `self` 中取出其 MRO 链(例如 `[Male, Person, object]`)。 2. 在该链条中定位第一个参数 `Male` 所在的位置。 3. 从 `Male` 紧随其后的那个类开始检索(跳过 `Male` 本身,第一个找到的是 `Person`)。 4. 检查 `Person` 中是否存在目标函数(如 `__init__`)。 5. 找到该函数后,将其绑定到 `self` 上并执行,其效果等同于调用 `Person.__init__(self, name)`。
为什么不直接硬编码写成 `Person.__init__(self, name)` 呢? - **代码重构与解耦**:如果未来基类名称发生变更(例如将 `Person` 改名为 `HomoSapiens`),或者修改了继承关系,直接使用硬编码名字会导致所有子类都需要逐处修改,极易引入 Bug;而使用 `super` 则会自动顺着 MRO 去寻找正确的类。 - **动态性机制(Dynamic Dispatch)**:很多教程不会讲到的核心原因在于,`super` 是完全动态的。“它会根据 self 的 MRO 进行寻找,而 self 也就是传进来的 argument 它本身是动态的。”在不改动当前函数源码的情况下,由于传入对象的不同,`super` 拿到的实际调用类也会动态发生改变。
---
[05:00](https://www.youtube.com/watch?v=bfuBVu7YALk&t=300s) 参数控制与类外调用黑魔法
为了进一步拆解两个参数的独立作用,高天引入了三层继承关系进行验证:`Male` 继承自 `Person`,而 `Person` 又继承自 `Animal`。在 `Animal` 中需要初始化参数 `age`。如果一切正常运行,`super(Male, self).__init__(age, name)` 会依次调用 `Person` 与 `Animal` 的初始化函数。
然而,若在第 15 行故意改写成 `super(Person, self).__init__(age, name)`,程序便会立刻报出 `TypeError`,提示 `__init__` 只需要两个位置参数。出现此现象的原因正是参数控制机制: - 当前 `self` 的完整 MRO 链是 `[Male, Person, Animal, object]`。 - 因为传入的第一个参数是 `Person`,`super` 会从 `Person` 之后开始查找,直接跳到了 `Animal`。 - `Animal.__init__` 只接收 `self` 和 `age` 两个参数,而代码多传了 `name`,导致报错。 - 如果将调用改为只传入 `age`(即 `super(Person, self).__init__(age)`),程序便会成功初始化出一个仅有 `age` 与 `gender`、跳过了 `Person` 初始化步骤的 `Male` 对象。
```mermaid flowchart LR accTitle: super(Person, self) 跳过 Person 类示例 accDescr: 展示在 MRO 链 Male -> Person -> Animal -> object 中,第一个参数指定为 Person 时会直接跳过 Person 检索 Animal
A["Male (实例 MRO 起点)"] --> B["Person (被第一个参数指定)"] B -.->|"跳过该类,从后一位查找"| C["Animal (命中 __init__)"] C --> D["object"] ```
这也证明了 `super` 绝非只能写在类定义(class definition)体内。只要掌握了它的双参数形态,在类的外部同样可以任意调用: - 在外部实例化一个未经初始化的对象 `m = Male.__new__(Male)` 后,可以直接在模块全局级别执行: ```python super(Male, m).__init__(18, "Peter") ``` - 此时 `super` 会顺着 `m` 的 MRO 链,越过 `Male` 查找到 `Person.__init__`,并成功绑定到对象 `m` 上完成所有父类属性的初始化。高天向观众抛出互动调查:“在看这期视频之前,你知道 super 可以在 class 之外使用吗?”
关于参数形态的进阶机制: - **单参数形式 `super(type)`**:返回的是一个未绑定对象(unbound object),必须在后续手动将其绑定到其他对象上。这种用法极为少见,涉及高级元编程与描述符(descriptor)机制。 - **无参数形式 `super()`**:只能安全地用于类定义内部。Python 在底层使用字节码注入等“黑魔法”自动补齐了这两个参数: 1. 寻找当前 `super()` 是在哪个 class 的作用域内被定义的,将其自动填入第一个参数。 2. 寻找当前方法执行时传入的第一个实参(通常是 `self` 或 `cls`),自动将其填入第二个参数。
---
[10:00](https://www.youtube.com/watch?v=bfuBVu7YALk&t=600s) 多继承下的动态调用陷阱
在多继承场景下,`super` 的动态性展示得最为淋漓尽致,也最容易颠覆程序员的直觉。
高天设计了一个经典的多继承示例。最初的代码结构非常线性: - 类 `A` 定义了方法 `c()`,输出 `"A"`。 - 类 `B` 继承自 `A`,在其 `c()` 方法中调用 `super().c()`。 - 类 `M` 继承自 `B`,在其 `c()` 方法中同样调用 `super().c()`。 - 当执行 `m = M(); m.c()` 时,调用链条平直无奇:`M.c -> B.c -> A.c`,控制台打印出 `"A"`。
```mermaid flowchart TD accTitle: 多继承 MRO 导致 super 调用平级类的机制 accDescr: 展示 M 继承 B 和 C,B 和 C 继承 A 时,M 的 MRO 链使得 B 中的 super().c() 调用了平级类 C 中的方法而非父类 A
subgraph Inheritance ["继承层次结构"] A_class["Class A (def c: print 'A')"] B_class["Class B (继承 A, 调用 super().c())"] C_class["Class C (继承 A, def c: print 'C')"] M_class["Class M (继承 B, C)"]
A_class --> B_class A_class --> C_class B_class --> M_class C_class --> M_class end
subgraph Execution ["m.c() 实际沿 MRO 执行流"] direction LR step1["M"] -->|"super()"| step2["B"] step2 -->|"super() 在 MRO 中向后找"| step3["C (打印 'C')"] step3 -->|"super()"| step4["A"] end ```
随后引入变化:定义一个新类 `C` 同样继承自 `A`,且 `C` 的 `c()` 方法打印 `"C"`。接着,让 `M` 同时继承 `B` 与 `C`(即 `class M(B, C): pass`)。此时整个类 `B` 的代码未做任何改动,`B` 的父类声明依旧清清楚楚写着是 `A`。
当调用 `m.c()` 时,终端打印出来的结果不是 `"A"`,而是 `"C"`!即使在 `M` 的定义中故意写死调用 `B.c(self)`,打印结果也依然是 `"C"`。
在单继承思维下,这个行为非常反直觉:类 `B` 的父类明明是 `A`,为什么在 `B` 里面写 `super().c()`,最终执行的却是八竿子打不着的平级类 `C` 的代码?
高天强调,核心原因正是之前阐述的原理: - 在 `B` 内部执行的 `super().c()`,等价于 `super(B, self).c()`。 - 关键在于此时传进去的 `self` 是子类 `M` 的实例,查找必须依据 **`self` 的 MRO 链条**。 - `M` 的 MRO 顺序是 `[M, B, C, A, object]`。 - `super(B, self)` 的第一参数是 `B`,因此它必须在 `M` 的 MRO 链中跳过 `B`,寻找排在 `B` **后面的下一个类**——这恰恰就是 `C`,而不是 `A`。
这就是面向对象编程中著名的协和多继承协同机制(Cooperative Multiple Inheritance)。“所以很多人会发现,自己对 super 的理解其实并不那么透彻,它并不是单纯地去找父类这么简单。”高天总结道,如果不深刻理解 Python 的 MRO 机制与 `super` 的双参数本质,在复杂继承体系中就会写出完全违背预期的代码。
## Notable quotes > [00:00](https://www.youtube.com/watch?v=bfuBVu7YALk&t=0s) 实际上 super 是一个正儿八经的 class,它是一个内置类的名字,然后 super 括号并不是调用了一个函数,super 括号是建立了一个 super 的对象。 — 码农高天
## Mentioned 码农高天, Python, CPython, MRO
## Video description 这期视频聊一下super,也是之前有人想让我讲的内容,正好可以用到前面的MRO的知识~
[Source](https://www.youtube.com/watch?v=bfuBVu7YALk)