本节摘要:继承组织的是属性查找路径,MRO 是这条路径的精确描述;多态在 Python 里不是类型系统的恩赐,而是"实现了同名方法就行"的鸭子类型;魔术方法则是用户类型接入语言协议的插座。本节把这三件事一次讲清,并给出继承与组合的取舍准则。
多继承时,属性到底从哪个类来?Python 用 C3 线性化把继承网压成一条确定的顺序——方法解析顺序(MRO)。直接看:
class Animal: def speak(self): return "..." def intro(self): return f"我是动物,我说{self.speak()}" class Dog(Animal): def speak(self): return "汪汪" class Bird(Animal): def speak(self): return "啾啾" def intro(self): return "我是一只鸟" class DogBird(Dog, Bird): pass db = DogBird() db.speak() # '汪汪':MRO 里 Dog 在前 db.intro() # '我是动物,我说汪汪':intro 沿 MRO 先找到 Animal 的版本!
最后一个结果常让人意外:intro 不是取自更"近"的 Bird,而是按 MRO 找到 Animal。打印线性化序列一切明了:
>>> [c.__name__ for c in DogBird.__mro__] ['DogBird', 'Dog', 'Bird', 'Animal', 'object']
MRO 保证两条不变量:子类永远在父类前、声明顺序被尊重,且满足单调性。若继承图无法线性化,类定义当场报错(菱形图中某些病态组合会抛 TypeError: Cannot create a consistent MRO)——错误在类定义期而非调用期爆发,这是 MRO 设计的贴心之处。

把 super() 理解成"调用父类"在单继承下没错,多继承下会误导。准确语义:沿实例的 MRO,找到当前类之后的下一个。菱形初始化的标准写法:
class Base: def __init__(self, **kw): print("Base ready") super().__init__(**kw) # object.__init__ 收尾 class LogMixin(Base): def __init__(self, *, verbose=False, **kw): self.verbose = verbose print("LogMixin ready") super().__init__(**kw) class Service(LogMixin, Base): def __init__(self, name, **kw): self.name = name print("Service ready") super().__init__(**kw) svc = Service("api", verbose=True) # Service ready → LogMixin ready → Base ready
**kw 透传 + 每层 super().__init__ 是多继承协作初始化的定式:每个类吃掉自己认识的参数、其余继续下传,object 收尾。谁少写了 super,链条就断在谁手里。
Java 的多态靠"实现同一接口"的类型声明;Python 只关心"运行时有没有这个方法":
class Duck: def speak(self): return "嘎嘎" class Robot: def speak(self): return "哔哔" class Person: def talk(self): return "你好" def announce(anything): return anything.speak() # 不查血统,只试方法 for actor in [Duck(), Robot()]: print(announce(actor)) # 嘎嘎 / 哔哔 announce(Person()) # AttributeError: 'Person' object has no attribute 'speak'
错误在调用点才爆——灵活与迟钝是同一枚硬币。工程上两条补丁:其一,用 hasattr/try 做能力探测(处理"可能没有"的场合);其二,用标准库的抽象基类给"有方法的约定"一个名字:
from abc import ABC, abstractmethod class Speaker(ABC): @abstractmethod def speak(self): ... class Robot(Speaker): def speak(self): return "哔哔" Robot() # 实现了 speak,可用 Speaker() # TypeError: 抽象类不能实例化
双下划线方法不是装饰,是解释器留好的插座:语法糖与内置函数在对象上的每一步,都会退到某个魔术方法。掌握它们,你的类型就能"像内置类型一样自然":
class Playlist: def __init__(self, *songs): self._songs = list(songs) def __len__(self): # len() 的插座 return len(self._songs) def __getitem__(self, i): # 下标与切片的插座,还顺带获得迭代能力 return self._songs[i] def __contains__(self, song): # in 运算符的插座 return song in self._songs def __repr__(self): # REPL 与 print 的显示插座 return f"Playlist{tuple(self._songs)!r}" pl = Playlist("晴天", "稻香", "夜曲") len(pl), pl[0], "夜曲" in pl # (3, '晴天', True) for s in pl: ... # 没写 __iter__ 也能迭代——3.1 节迭代协议的续篇
__getitem__ 免费送迭代能力是迭代协议的美妙推论:旧式协议规定"按下标从 0 取,直到 IndexError"。而实现 __iter__ + __next__ 才是现代做法(第 7.1 节细讲)。
两个必须配套的插座——__eq__ 与 __hash__(呼应 2.2 节的键规则):
class Point: def __init__(self, x, y): self.x, self.y = x, y def __eq__(self, other): return isinstance(other, Point) and (self.x, self.y) == (other.x, other.y) def __hash__(self): return hash((self.x, self.y)) # 相等对象哈希必须相等 p = Point(1, 2) seen = {Point(1, 2): "origin", p: "again"} # 值相等的键合并 len(seen) # 1
只写 __eq__ 不写 __hash__,实例立刻不可哈希、进不了 set 和 dict 键——解释器替你把一致性兜住了。常用插座速查:__str__/__repr__(显示)、__add__ 等(运算符)、__call__(实例可调用)、__enter__/__exit__(with 协议,第 6 章)、__iter__(迭代协议,第 7 章)。
继承的正当用途只有两个:真正的"是一个"关系 与复用接口/框架扩展点(如异常体系、ABC)。为了"偷几行代码"而继承,会把两个本无关系的类焊死,父类一改全家族震动。优先组合:
class Timestamps: # 能力类:Mixin created_at = None def touch(self): import time self.created_at = time.time() class Article: # 组合/混入,而非继承某个"文章基类" def __init__(self, title): self.title = title self.ts = Timestamps() a = Article("标题"); a.ts.touch()
⚠️ 常见坑:多继承里两分支各自
super可能重复执行某个祖先(C3 已处理菱形去重,但漏写 super 会断链);在__eq__里用==递归比较自己;重写__str__忘了__repr__(调试时看到一堆没信息量的对象显示)。
💡 关键直觉:不要问"它是不是 Dog",问"它会不会 speak"。设计类时先设计魔术方法与协议,属性只是协议背后的存储。
__mro__ 可查、病态图定义期报错。**kw 透传 + 逐层 super().__init__ 是协作初始化定式。__len__、__getitem__、__contains__、__repr__ 让自定义类型融入语言。__eq__ 与 __hash__ 配套:解释器强制一致性,值语义对象两者都写。第 5 章把镜头拉到比类更大的组织单位——模块与包,以及为什么"模块即命名空间"意味着属性查找故事还有上半场。