前言
绝大多数面向对象语言只支持单继承:一个类只能有一个直接父类。Python 是少数原生支持多重继承的主流语言——一个类可以同时继承多个基类,写成class C(A, B): ...。
多重继承的强大之处在于可以把若干"能力片段"拼装进一个类,典型用法就是 Mixin(混入类)。但它的代价也很明显:方法到底走哪条链,不能光看继承列表就想清楚;一旦某个环节没有"接力",方法会被静默跳过,排查起来相当痛苦。
围绕多重继承最常见的误解,是把super()当成"调用父类"。在单继承里这么理解勉强够用,但在多重继承里super()取的是方法解析顺序(MRO)中的下一个类,它可能与书写它的类并没有继承关系。正因为这个语义,多个类才能像接力一样协作——前提是每一环都按约定把调用传下去。
本文讲多重继承的语法与菱形问题、协作式super()的写法(含**kwargs参数接力),以及 Mixin 的定位和不该用多重继承的场合。顺序由 C3 线性化决定,算法的细节不在本文展开,本文只把它当作一条既定的查找序列来使用。
一、语法与"钻石问题"
多重继承的语法只是把多个基类写进括号里,从左到右排列:
# 适用于 Python 3.8+
class Node:
def describe(self):
return "Node"
class Tagged:
def describe(self):
return "Tagged"
class Document(Node, Tagged):
pass
print(Document().describe()) # Node两个基类都定义了describe,结果是Node的版本——因为Node写在前面。但只要继承结构里出现"菱形(diamond)",顺序就不再由这一行决定。
补充一句历史:Python 2 时代如果写成class Document(Node, Tagged):而基类没有继承object,得到的是旧式类,多重继承的解析规则与新式类不同,这也是当年到处要补object的原因。Python 2.7 已于 2020 年 1 月 1 日停止维护;Python 3 里所有类都是新式类,不必再补object。
所谓菱形,是指两条继承路径最终汇合到同一个祖先(所有类都继承object,所以任何多重继承都天然构成菱形):
Entity
/ \
Logger Serializer
\ /
Record若按"深度优先"逐个去找,这个汇合点可能被访问两次。Python 的做法是把整条链排成一个不重复的序列,每个类只出现一次。
二、协作式 super():把调用接力下去
在多重继承语境里,super()取的是MRO 中当前类之后的下一个类,不是"父类"。例如__mro__是Record -> LoggerMixin -> SerializerMixin -> Entity -> object时,在LoggerMixin里调用super(),是从SerializerMixin开始往后找——尽管LoggerMixin的直接父类其实是Entity。
正因为super()认的是"下一个",多个类才能配合完成同一件事:每个类的__init__只负责初始化自己关心的那部分,然后调用super().__init__(**kwargs),把剩下的参数交给下一站。这种写法叫协作式多重继承(cooperative multiple inheritance)。
关键技巧是用**kwargs接力:每一环只取走自己需要的参数,把其余的原样传给下一站,链路走到最后时字典恰好为空,交给object.__init__()。
# 适用于 Python 3.8+
class Entity:
def __init__(self, **kwargs):
self.initialized = True
super().__init__(**kwargs)
class LoggerMixin(Entity):
def __init__(self, log_lines=None, **kwargs):
self.log_lines = list(log_lines or ())
super().__init__(**kwargs)
class SerializerMixin(Entity):
def __init__(self, fields=(), **kwargs):
self.fields = tuple(fields)
super().__init__(**kwargs)
class Record(LoggerMixin, SerializerMixin):
def __init__(self, name, **kwargs):
self.name = name
super().__init__(**kwargs)
r = Record(name="alice", fields=("id", "name"), log_lines=("created",))
print([c.__name__ for c in Record.__mro__])
# ['Record', 'LoggerMixin', 'SerializerMixin', 'Entity', 'object']
print(r.name, r.fields, r.log_lines, r.initialized)
# alice ('id', 'name') ['created'] True把参数逐层拆开看:Record取走name,LoggerMixin取走log_lines,SerializerMixin取走fields,到Entity时只剩空字典。Entity是菱形里的公共基类,两条路径都通向它,但因为 MRO 里它只出现一次、每一环又都调用了super(),它只被初始化一次。
如果不按这套约定写,问题会以两种面貌出现。一是公共基类被初始化两次:
# 适用于 Python 3.8+
class Entity:
count = 0
def __init__(self):
Entity.count += 1
class LoggerMixin(Entity):
def __init__(self):
self.log_lines = []
Entity.__init__(self) # 点名基类,绕过了接力
class SerializerMixin(Entity):
def __init__(self):
self.fields = []
Entity.__init__(self) # 又点一次
class Record(LoggerMixin, SerializerMixin):
def __init__(self, name):
self.name = name
LoggerMixin.__init__(self)
SerializerMixin.__init__(self)
Record("alice")
print(Entity.count) # 2 —— 公共基类被初始化了两次二是参数丢失或直接报错:如果中间某一环写的是def __init__(self, log_lines):而没有**kwargs,上游传下来的fields就无处安放,调用时会抛TypeError;若这一环干脆不调用super().__init__(**kwargs),它下游的所有初始化都会静默跳过——程序不报错,只在运行到缺少的属性时才暴露。
一条成熟的协作链应当满足三条:super()指向的方法确实存在、调用方与被调方签名兼容(靠**kwargs兜底)、以及每个环节都调用super()。这正是 Mixin 要写成协作式的原因——只有这样,它们才能被任意组合、任意调换顺序。
三、Mixin 模式:多重继承最实用的用法
Mixin 是一个不打算单独实例化、只为提供某项能力的类。命名上常以Mixin结尾,它通常不定义__init__;若确实需要定义,就必须写成协作式——接收**kwargs并用super().__init__(**kwargs)透传,否则会打断上一节的接力。
# 适用于 Python 3.8+
import json
class JsonMixin:
"""给任意类加上 to_json 能力。"""
def to_json(self):
return json.dumps(self.__dict__, ensure_ascii=False)
class TimestampMixin:
def timestamp(self):
return self.created_at
class User(JsonMixin, TimestampMixin):
def __init__(self, name, created_at):
self.name = name
self.created_at = created_at
u = User("alice", "2026-10-07")
print(u.to_json()) # {"name": "alice", "created_at": "2026-10-07"}
print(u.timestamp()) # 2026-10-07Mixin 的好处是能力可以像零件一样组合。代价是当 mixin 之间、或 mixin 与主类之间出现同名方法时,谁是最终生效的那个、构造时谁的__init__先跑,都由 MRO 决定,而 MRO 又受各处继承结构影响,不易一眼看出。
下表对比几种把能力组合起来的方式:
| 方式 | 关系语义 | 优点 | 主要风险 |
|---|
| 单继承 | is-a | 简单直观 | 不能复用不相关的能力 |
| 多重继承 / Mixin | is-a + 能力拼装 | 复用灵活 | 顺序不直观,漏掉super()会被静默跳过 |
| 组合(has-a) | 包含 | 耦合低、边界清晰 | 需要转发方法 |
| 动态添加属性 | 运行时扩展 | 无需改类定义 | 类型检查器不认,难维护 |
四、什么时候不该用多重继承
多重继承不是越多越好。出现下面任一信号时,优先考虑组合:
- 两个基类之间没有语义上的"是一种"关系,只是凑巧都要用到某个方法;
- 需要靠
isinstance(self, SomeClass)在方法里区分具体走哪条分支; - MRO 已经长到需要打印出来才敢改代码;
- 多个基类的
__init__参数互相冲突,子类为了接力要给同一件事传两次。
还有一类信号来自继承结构本身:当基类顺序自相矛盾(例如把一个祖先排在它的后代之后)时,线性化根本算不出可用序列,Python 会直接抛TypeError: Cannot create a consistent method resolution order (MRO)。这属于线性化算法层面的问题,本文不展开;但可以把它当成一个明确的提示——需要重构层次,而不是去微调写法。
常见坑点
1.super()写死成具体父类名,打断接力
❌ 在LoggerMixin.__init__里写Entity.__init__(self, **kwargs),直接点名基类,菱形里的公共基类在每条路径上都会被再进一次。
✅ 写super().__init__(**kwargs),把"下一站是谁"交给 MRO 决定。
2. 中间某一环不接收**kwargs,参数丢失或报错
❌def __init__(self, log_lines):不接受多余的关键字参数,上游传来的fields要么无处安放、要么直接抛TypeError。
✅ 只声明自己需要的参数,其余用**kwargs接住:def __init__(self, log_lines=None, **kwargs)。
3. 忘了调用super().__init__,公共基类不再初始化
❌ 某个 mixin 里只写self.log_lines = []就结束,下游(包括公共基类)的初始化全部静默跳过,且不报错。
✅ 每个参与协作的__init__末尾都调用super().__init__(**kwargs),哪怕自己没有多余的参数要传。
4. 以为 mixin 的组合顺序无所谓
❌ 断定class Record(LoggerMixin, SerializerMixin)与class Record(SerializerMixin, LoggerMixin)行为完全相同。
✅ 左右顺序会改变__mro__,既影响__init__的接力次序,也影响同名方法谁生效;按需要排好序后打印__mro__确认。
5. 两个 mixin 抢同一个__init__参数名
❌LoggerMixin与SerializerMixin都写def __init__(self, name, **kwargs),而两者对name的含义不同,接力时同一参数互相覆盖。
✅ 各 mixin 只用自己命名空间下的关键字参数(如log_lines、fields),或干脆改用组合。
6. mixin 依赖self上并不存在的属性
❌TimestampMixin.timestamp直接返回self.created_at,但某个使用它的类根本没有created_at,只有运行到那一行才炸。
✅ mixin 要么只依赖自己声明用到的接口,要么在使用文档里写清前置条件。
7. 把非空的**kwargs原样丢给object.__init__
❌ 协作链最后一环没有取走自己的参数,object.__init__(**kwargs)收到多余关键字,抛TypeError。
✅ 每一环都取走自己需要的参数;链路走到object时应是空字典,这正是"每层都要拆参数"的意义。
总结
| 关注点 | 结论 |
|---|
super()含义 | MRO 中的下一个类,不是"父类" |
| 协作式写法 | 每层取走自己的参数,super().__init__(**kwargs)透传其余 |
| 公共基类 | 菱形下靠协作只初始化一次;写死父类名则会重复 |
| Mixin 定位 | 提供能力,不单独实例化,定义__init__时必须透传参数 |
| 组合 vs 继承 | 没有 is-a 关系时用组合 |
多重继承是一把需要配合super()才能安全使用的工具:用对了,Mixin 能让能力像积木一样拼装;用错了,方法会被静默跳过、公共基类被重复初始化,问题直到运行时才暴露。写协作方法时记住一条铁律——每一环都取走自己的参数、并把super()接力下去,就能避开绝大多数陷阱。