R

ReflexKR

Reflex 官方文档

名牌机场:Python 对象模型

·

课程概览

1

标签不是箱子:名字与对象

2

封口的箱子:可变与不可变

3

箱中箱:容器与复制

4

交出箱子的时候:函数与参数

5

区域与通行证:作用域

6

随身行李:闭包

7

箱子规格书:类与实例

8

机场标准流程:双下划线方法

9

规格书的规格书:继承与 MRO

10

安检口:属性访问的秘密

11

无主的箱子:内存与 GC

12

储物柜号码:哈希与相等,综合

名牌机场:Python 对象模型
第9课

时间 6:40–7:30

规格书的规格书:继承与 MRO

失物报告单

LOST-009

两条规定少了一条

报告人

航空货运规定开发者 韩科长

丢了什么

充电宝规格 PowerBank 的危险品检查。让它同时继承了“随身携带”规定和“危险品”规定,可检查结果里没有危险品这一项。

结案条件

查明 PowerBank 读取规定的顺序(MRO),并改成两条规定都会执行。

天空岛国际机场 行李中心

韩科长新做了一份充电宝规格。既然让它同时继承了随身携带规格书和危险品规格书,本以为两项检查都会做。可检查表上只有尺寸检查,没有危险品检查。

都美兰班长把两张规格书叠在一起看。“上级规格书有两份的时候,先读哪一份是有规定的。先从这个顺序看起。”

9-1. 上级规格书:继承与重写(7 分钟)

class CarryOn(Item): 的意思是“CarryOn 里没有的,去 Item 里找”。这个查找顺序写在类的 __mro__(Method Resolution Order,方法解析顺序)里。

class Item:
    kind = "item"
    def label(self):
        return f"[{self.kind}]"

class CarryOn(Item):
    kind = "carry-on"

b = CarryOn()
print(b.label())
# [carry-on]
print(CarryOn.__mro__)
# (<class '__main__.CarryOn'>, <class '__main__.Item'>, <class 'object'>)
print("label" in vars(CarryOn), "label" in vars(Item))
# False True

读取 b.label() 的过程和第 7 课一样。实例的 __dict__ 里没有,就从前往后扫 __mro__。CarryOn 里没有 label,在 Item 里找到了。而 label 里的 self.kind 又会从实例开始找,所以得到的是 CarryOn.kind,也就是 "carry-on"。

重写(override)不是什么特殊语法。在子规格书里写上同一个名字,在 MRO 中先找到的那个就赢。想接着用父类的版本,就用 super() 调用。

class Fragile(Item):
    kind = "fragile"
    def label(self):
        return super().label() + "!"

print(Fragile().label())
# [fragile]!

所有类的 MRO 都以 object 结尾。__repr__、__eq__ 这些默认把手都来自那里(第 8 课)。

9-2. 多重继承与菱形(8 分钟)

报告人的规格继承了两份规格书。它们又都继承自 Item,画出来就是一个菱形(钻石)。

class Item:
    rule = "item"
    tag = "plain"
class CarryOn(Item):
    rule = "carry-on"
class Hazmat(Item):
    rule = "hazmat"
    tag = "red"
class PowerBank(CarryOn, Hazmat):
    pass

print([c.__name__ for c in PowerBank.__mro__])
# ['PowerBank', 'CarryOn', 'Hazmat', 'Item', 'object']
print(PowerBank.rule, PowerBank.tag)
# carry-on red
  • rule 先在 CarryOn 里找到了。括号里左边的父类优先。
  • tag 在 CarryOn 里没有,于是在下一格 Hazmat 里找到了,不会一路下到 Item 的 "plain"。
  • Item 只出现一次,而且是在两个子类都排完之后才出现。这不是沿着左边一路挖到底的深度优先。

在下面的探索器里调换父类顺序看看,也点一下最后那种排不出顺序的规格。

MRO 探索器

要找的属性

查找顺序(__mro__)

哪里都没有

C3 计算步骤
(CarryOn, Hazmat) 中 rule 在 CarryOn 里找到,(Hazmat, CarryOn) 中则在 Hazmat 里找到。(Item, Hazmat) 无法生成 MRO,类根本创建不出来。

9-3. super() 不是父类,而是下一格(10 分钟)

现在来看报告人真正的代码。每条规定返回自己的检查项目,再接上上级规定的项目。

class Item:
    def check(self):
        return ["item"]
class CarryOn(Item):
    def check(self):
        return ["size"] + Item.check(self)
class Hazmat(Item):
    def check(self):
        return ["fire"] + super().check()
class PowerBank(CarryOn, Hazmat):
    pass

print(PowerBank().check())
# ['size', 'item']

写 CarryOn 的人心想“我的父类是 Item”,就直接调用了 Item.check(self)。单独使用时这没错。可在 PowerBank 的 MRO 里,CarryOn 的下一格不是 Item,而是 Hazmat。直接调用,就把 Hazmat 跳过去了。

super() 不是“我的父类”,而是在 type(self) 的 MRO 中,当前类的下一格。同样是 CarryOn.check,由谁来调用,下一格就不一样。

class CarryOn(Item):
    def check(self):
        return ["size"] + super().check()
class PowerBank(CarryOn, Hazmat):
    pass

print(PowerBank().check())
# ['size', 'fire', 'item']
print(CarryOn().check())
# ['size', 'item']
print(super(CarryOn, PowerBank()).check())
# ['fire', 'item']
  • 在 PowerBank() 中,链条按 CarryOn → Hazmat → Item 的顺序接下去。
  • 单独的 CarryOn(),MRO 是 CarryOn, Item, object,所以 super() 就是 Item。
  • super(CarryOn, obj) 是把“在 obj 的 MRO 里从 CarryOn 的下一格开始找”明确写了出来。不带参数的 super() 会自动填好这两个值。

在下面的标签板上比较两种写法。看调用栏怎么堆起来,以及正在执行的行号。

标签板

步骤 1 / 0

下一行将执行: 0

正在准备实验台…

刚刚变化的

封口的箱子(不可变)

在 super() 中,行号按 6 → 9 → 3 往下走,堆起三个 check 栏,三栏里的 self 都是同一个 PowerBank 箱子。在 Item.check(self) 中,直接从 6 → 3,只有两栏。用了 super() 的函数身上带着 __class__ cell,它告诉 super()“现在在哪个类里面”。

链条要每一个人都调用 super() 才能接下去。只要有一个人漏掉,后面的格子就不会执行。__init__ 也一样。如果每份规格书接收的参数不同,就只取出自己的,其余的用 **kw 传给下一格。

class Item:
    pass
class CarryOn(Item):
    def __init__(self, cm=55, **kw):
        self.cm = cm
        super().__init__(**kw)
class Hazmat(Item):
    def __init__(self, wh=100, **kw):
        self.wh = wh
        super().__init__(**kw)
class PowerBank(CarryOn, Hazmat):
    pass

print(vars(PowerBank(cm=20, wh=99)))
# {'cm': 20, 'wh': 99}

CarryOn.__init__ 只取出 cm,把 wh=99 传下去。下一格 Hazmat 接住它,最后 object.__init__() 以空参数收尾。

小钉提示

→ 在方法里直接写父类名来调用(Item.check(self)),多重继承时就会跳过夹在中间的规格书。要接着往下调用,一律用 super()。

9-4. C3 线性化:决定顺序的规则(6 分钟)

Python 用一种叫 C3 线性化的方法生成 MRO。它遵守三条约定:

  1. 子类永远排在父类前面。
  2. 遵守括号里写的父类顺序。
  3. 也遵守每个父类自己的 MRO 顺序。

PowerBank(CarryOn, Hazmat) 的计算过程如下。把各父类的 MRO 和父类列表并排放好:

剩余列表选出理由
[CarryOn, Item, object] [Hazmat, Item, object] [CarryOn, Hazmat]CarryOn是第一个列表的头,且不在其他列表的尾部
[Item, object] [Hazmat, Item, object] [Hazmat]HazmatItem 在第二个列表的尾部,所以往后推
[Item, object] [Item, object]Item现在不在尾部了
[object] [object]object

结果是 PowerBank 后面跟着 CarryOn, Hazmat, Item, object。展开探索器里的“C3 计算步骤”,能看到同样的过程分成四行。每行最前面是这一轮选出的类,后面是当时剩下的列表。

如果三条约定无法同时满足,类就创建不出来。

class Item:
    pass
class Hazmat(Item):
    pass
class Odd(Item, Hazmat):
    pass
# TypeError: Cannot create a consistent method resolution order (MRO) for bases Item, Hazmat

按括号顺序,Item 要排在 Hazmat 前面(约定 2);按子类优先的规则,Hazmat 又要排在 Item 前面(约定 1)。没有一种排法能同时满足两者。修正很简单:把更具体的父类放在左边。

9-5. Mixin、isinstance 与组合(6 分钟)

Mixin(混入类)是一种不单独使用、专门往别的类里掺进一项功能的小类。名字末尾加上 Mixin,放在父类列表的左边,这样它在 MRO 里才会先被找到。

class ReprMixin:
    def __repr__(self):
        fields = ", ".join(f"{k}={v!r}" for k, v in vars(self).items())
        return f"{type(self).__name__}({fields})"

class Tag(ReprMixin):
    def __init__(self, code, kg):
        self.code, self.kg = code, kg

print([Tag("ICN", 12)])
# [Tag(code='ICN', kg=12)]

MRO 探索器

要找的属性

查找顺序(__mro__)

哪里都没有

C3 计算步骤
LabelMixin 放在左边时,describe 在 Mixin 里找到;放在右边时,先找到的是 Item 的,Mixin 的 describe 被遮住了。不重名的 prefix 放在哪边都能找到。

isinstance(obj, C) 看的是 type(obj) 的 MRO 里有没有 C。issubclass 在类与类之间问同样的问题。所以它比 type(x) is C 的范围更宽。

class Item:
    pass
class Hazmat(Item):
    pass
class PowerBank(Hazmat):
    pass

pb = PowerBank()
print(isinstance(pb, Item), issubclass(PowerBank, Hazmat), isinstance(pb, (int, Hazmat)))
# True True True
print(type(pb) is Hazmat)
# False

继承用于“A 是 B 的一种”的关系。如果是“A 拥有 B”,组合(把别的对象作为属性拿着)更合适。报告人的规定如果不靠继承,而是作为一个规定对象列表拿着,顺序一目了然,也就不必去琢磨 MRO 了。

class Rule:
    def __init__(self, name, test):
        self.name, self.test = name, test

class Inspector:
    def __init__(self, *rules):
        self.rules = list(rules)
    def failed(self, item):
        return [r.name for r in self.rules if not r.test(item)]

size = Rule("size", lambda it: it["cm"] <= 55)
fire = Rule("fire", lambda it: it["wh"] <= 100)
print(Inspector(size, fire).failed({"cm": 20, "wh": 160}))
# ['fire']

9-6. 抽象规格书与子规格登记(3 分钟)

继承 abc.ABC 并加上 @abstractmethod,就成了“子类必须填上的空格”。没填的子类无法创建实例。__init_subclass__ 是在子类每次被创建时由父类调用的把手,可以用来做规格登记簿。

from abc import ABC, abstractmethod

class Rule(ABC):
    registry = []
    def __init_subclass__(cls, **kw):
        super().__init_subclass__(**kw)
        Rule.registry.append(cls.__name__)
    @abstractmethod
    def check(self, item): ...

class SizeRule(Rule):
    def check(self, item):
        return item["cm"] <= 55
class FireRule(Rule):
    pass

print(Rule.registry, SizeRule().check({"cm": 40}))
# ['SizeRule', 'FireRule'] True
FireRule()
# TypeError: Can't instantiate abstract class FireRule without an implementation for abstract method 'check'

结案(10 分钟)

把 CarryOn 里的 Item.check(self) 换成 super().check(),再连同 MRO 一起重新打印检查表。

class Item:
    def check(self):
        return ["item"]
class CarryOn(Item):
    def check(self):
        return ["size"] + super().check()
class Hazmat(Item):
    def check(self):
        return ["fire"] + super().check()
class PowerBank(CarryOn, Hazmat):
    pass

print([c.__name__ for c in PowerBank.__mro__])
# ['PowerBank', 'CarryOn', 'Hazmat', 'Item', 'object']
print(PowerBank().check())
# ['size', 'fire', 'item']

fire 回到了检查表上。韩科长点点头:“该叫的不是父类,而是下一个。”班长把两张规格书重新装订好:“顺序由 Python 来定。我们只要别把链条弄断就行。”

结案报告 — LOST-009。CarryOn 直接调用了父类名,跳过了 MRO 中的下一格 Hazmat。改成所有规格书都调用 super(),问题解决。

课后练习

  1. 对 class A: pass、class B(A): pass、class C(A): pass、class D(C, B): pass,先用 C3 表格算出 MRO,再用 D.__mro__ 确认。
  2. 在结案代码中,只把 Hazmat.check 改成 return ["fire"](不调用 super()),预测检查表会变成什么样。
  3. 给 9-5 的 Inspector 再加一条重量规定,调换规定顺序,看看结果列表的顺序怎么变。

班长的工作手册

  • 属性按 Cls.__mro__ 的顺序查找。重写不过是先找到的赢。

  • 多重继承中,共同的父类只出现一次,排在所有子类之后(C3 线性化)。

  • super() 不是父类,而是 type(self) 的 MRO 中的下一格。每个人都调用,链条才接得上。

  • __init__ 链条只取出自己的参数,其余用 **kw 传下去。

  • 生成不了 MRO,类就根本创建不出来。更具体的父类放左边。

  • Mixin 放左边。isinstance 看的是 MRO。“拥有”关系用组合,而不是继承。

← 上一课

第8课 · 机场标准流程:双下划线方法

下一课 →

第10课 · 安检口:属性访问的秘密

订阅新课时和新课程通知

本课程新增课时或推出后续课程时,我们会发邮件通知你。

收集项目:电子邮箱 · 用途:本课程的新课时和上线通知 · 保存期限:直到你要求删除。不会用于其他用途,也不会提供给他人。回复任意通知邮件,我们会立即删除。

问答

关于本课有任何疑问都可以在这里留言。所有语言版本页面的提问会汇集在一起。

以纯文本显示,不支持链接和格式。

R

ReflexKR

动手学 Python 和 Reflex。所有示例均已在 Reflex 0.10.0 上实际运行验证。

ReflexKR 免费,没有横幅广告。如果对你有帮助,欢迎请我们喝杯咖啡。 ☕ 请我们喝杯咖啡

联盟链接与赞助说明
한국어English日本語简体中文EspañolDeutsch

© 2026 ReflexKR · Since 2026 · CC BY-NC 4.0

Built with Reflex