失物报告单
LOST-009
报告人
航空货运规定开发者 韩科长
丢了什么
充电宝规格 PowerBank 的危险品检查。让它同时继承了“随身携带”规定和“危险品”规定,可检查结果里没有危险品这一项。
结案条件
查明 PowerBank 读取规定的顺序(MRO),并改成两条规定都会执行。
天空岛国际机场 行李中心
韩科长新做了一份充电宝规格。既然让它同时继承了随身携带规格书和危险品规格书,本以为两项检查都会做。可检查表上只有尺寸检查,没有危险品检查。
都美兰班长把两张规格书叠在一起看。“上级规格书有两份的时候,先读哪一份是有规定的。先从这个顺序看起。”
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 课)。
报告人的规格继承了两份规格书。它们又都继承自 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 redrule 先在 CarryOn 里找到了。括号里左边的父类优先。tag 在 CarryOn 里没有,于是在下一格 Hazmat 里找到了,不会一路下到 Item 的 "plain"。Item 只出现一次,而且是在两个子类都排完之后才出现。这不是沿着左边一路挖到底的深度优先。在下面的探索器里调换父类顺序看看,也点一下最后那种排不出顺序的规格。
MRO 探索器
要找的属性
查找顺序(__mro__)
哪里都没有
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() 才能接下去。只要有一个人漏掉,后面的格子就不会执行。__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()。
Python 用一种叫 C3 线性化的方法生成 MRO。它遵守三条约定:
PowerBank(CarryOn, Hazmat) 的计算过程如下。把各父类的 MRO 和父类列表并排放好:
| 剩余列表 | 选出 | 理由 |
|---|---|---|
[CarryOn, Item, object] [Hazmat, Item, object] [CarryOn, Hazmat] | CarryOn | 是第一个列表的头,且不在其他列表的尾部 |
[Item, object] [Hazmat, Item, object] [Hazmat] | Hazmat | Item 在第二个列表的尾部,所以往后推 |
[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)。没有一种排法能同时满足两者。修正很简单:把更具体的父类放在左边。
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__)
哪里都没有
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']继承 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'把 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(),问题解决。
class A: pass、class B(A): pass、class C(A): pass、class D(C, B): pass,先用 C3 表格算出 MRO,再用 D.__mro__ 确认。Hazmat.check 改成 return ["fire"](不调用 super()),预测检查表会变成什么样。Inspector 再加一条重量规定,调换规定顺序,看看结果列表的顺序怎么变。班长的工作手册
属性按 Cls.__mro__ 的顺序查找。重写不过是先找到的赢。
多重继承中,共同的父类只出现一次,排在所有子类之后(C3 线性化)。
super() 不是父类,而是 type(self) 的 MRO 中的下一格。每个人都调用,链条才接得上。
__init__ 链条只取出自己的参数,其余用 **kw 传下去。
生成不了 MRO,类就根本创建不出来。更具体的父类放左边。
Mixin 放左边。isinstance 看的是 MRO。“拥有”关系用组合,而不是继承。
订阅新课时和新课程通知
本课程新增课时或推出后续课程时,我们会发邮件通知你。
问答
关于本课有任何疑问都可以在这里留言。所有语言版本页面的提问会汇集在一起。