多重继承
多重继承允许一个类同时继承多个父类:class Child(Left, Right):。子类可以组合多个类提供的行为,但也会带来方法冲突、初始化顺序和共同祖先重复调用等问题。
Python 的多重继承并不是“随机挑一个父类”。它使用方法解析顺序(Method Resolution Order,MRO),按照一条确定的线性顺序查找属性和方法。只要所有参与者都遵守协作式 super() 约定,多重继承就可以成为组合功能的实用工具;如果父类彼此不了解、签名不兼容,组合通常会变得脆弱。
本文示例适用于 Python 3.10 及以上版本。学完后,你应该能够:
- 声明多重继承关系并读取类的 MRO;
- 解释
super()为什么不是简单的“调用某个直接父类”; - 使用 mixin 和关键字参数构建协作式初始化链;
- 处理菱形继承、方法冲突、槽位冲突和不兼容父类;
- 判断多重继承是否适合当前设计。
先看一个完整例子
下面的类把“记录创建时间”和“转换为字典”作为两个独立能力,通过 mixin 组合到实体类中。每个 __init__() 都把自己消费的参数取出后,使用 super() 把其余参数交给 MRO 中的下一个类:
from datetime import datetime, timezone
class Entity:
def __init__(self, *, name: str, **kwargs):
if kwargs:
unexpected = ", ".join(kwargs)
raise TypeError(f"未使用的参数:{unexpected}")
self.name = name
class TimestampMixin:
def __init__(self, **kwargs):
self.created_at = datetime.now(timezone.utc)
super().__init__(**kwargs)
class DictMixin:
def as_dict(self) -> dict[str, object]:
return {
"name": self.name,
"created_at": self.created_at.isoformat(),
}
def __init__(self, **kwargs):
super().__init__(**kwargs)
class User(TimestampMixin, DictMixin, Entity):
pass
user = User(name="小林")
print(User.__mro__)
print(user.as_dict())
User 的初始化链是 TimestampMixin.__init__() → DictMixin.__init__() → Entity.__init__()。每个类只做自己的工作,不直接点名下一个父类,因此以后调整 mixin 顺序时,链条仍会按照新的 MRO 工作。
声明和查找多重继承
基本语法
类定义的括号中可以列出多个父类:
class Printable:
def print_preview(self) -> str:
return "预览"
class Exportable:
def export(self) -> str:
return "导出"
class Report(Printable, Exportable):
pass
report = Report()
print(report.print_preview())
print(report.export())
当访问 report.export 时,Python 会先查看实例,再按照类的 MRO 依次查看 Report、Printable、Exportable 以及共同基类。MRO 可以通过以下方式查看:
print(Report.__mro__)
print(Report.mro())
两者都会返回包含 Report 和 object 的类型序列。排查“到底调用了哪个方法”时,先查看 MRO 往往比凭直觉猜测更可靠。
左侧父类优先,但不只是简单的从左到右
没有更复杂的继承关系时,左侧父类的方法通常先被找到:
class Left:
label = "左"
class Right:
label = "右"
class Child(Left, Right):
pass
print(Child().label) # 左
但 Python 还要保证每个类只出现一次,并且保留局部继承顺序。复杂层次由 C3 线性化算法计算;如果无法同时满足这些约束,类定义会抛出 TypeError,而不是生成一个含糊的 MRO。
super() 与 MRO
super() 表示 MRO 中的下一个实现
在 Child 的方法中调用 super(),含义是“从当前类在 MRO 中的位置继续查找”,而不是“无条件调用某个写在括号里的父类”:
class A:
def greet(self) -> str:
return "A"
class B(A):
def greet(self) -> str:
return "B -> " + super().greet()
class C(A):
def greet(self) -> str:
return "C -> " + super().greet()
class D(B, C):
def greet(self) -> str:
return "D -> " + super().greet()
print(D().greet()) # D -> B -> C -> A
print(D.__mro__) # D, B, C, A, object
B.greet() 中的 super() 看到的不是固定的 A,而是 D 的 MRO 中 B 后面的 C。这就是多重继承能够让所有分支参与同一次操作的关键。
为什么不直接调用父类名称
下面的写法会绕过 MRO 中的其他类:
class BadB(A):
def greet(self) -> str:
return "B -> " + A.greet(self) # 直接锁定 A
在单继承中,显式写 Parent.method(self) 有时还能工作;在多重继承中,它可能导致某个 mixin 被跳过、共同祖先重复执行,或者让类顺序调整后出现隐蔽错误。协作式继承的实例方法和初始化方法通常都应使用 super()。
菱形继承
经典结构
菱形继承指两个父类共享同一个基类:
class Root:
def run(self) -> str:
return "Root"
class LeftBranch(Root):
def run(self) -> str:
return "Left(" + super().run() + ")"
class RightBranch(Root):
def run(self) -> str:
return "Right(" + super().run() + ")"
class Diamond(LeftBranch, RightBranch):
def run(self) -> str:
return "Diamond(" + super().run() + ")"
print(Diamond().run()) # Diamond(Left(Right(Root)))
Root.run() 只执行一次,因为 MRO 是 Diamond, LeftBranch, RightBranch, Root, object。如果 LeftBranch 或 RightBranch 直接调用 Root.run(self),就可能绕过另一条分支或重复调用根方法。
协作式方法的规则
希望方法链完整运行时,每个参与类都应遵守以下规则:
- 使用
super()调用下一个实现; - 调用前后只处理自己负责的逻辑;
- 保持兼容的参数签名,并转发未消费的参数;
- 不假设
super()一定指向某个固定的具体父类; - 文档中说明该方法是协作式的,避免子类静默截断链条。
如果一个父类无法修改,也不接受 super(),就不能假设整条链会协作。此时需要隔离它、显式调用它,或改用组合。
用 mixin 组合小能力
mixin 应该是什么
mixin 通常表示一项可复用能力,而不是一个完整的业务实体。它往往不单独实例化,也不负责完整的对象身份,只提供一组方法或很小的初始化逻辑:
import json
class JsonMixin:
def to_json(self) -> str:
return json.dumps(self.to_dict(), ensure_ascii=False)
def to_dict(self) -> dict[str, object]:
raise NotImplementedError
class User(JsonMixin):
def __init__(self, name: str):
self.name = name
def to_dict(self) -> dict[str, object]:
return {"name": self.name}
print(User("小林").to_json())
常见 mixin 名称会以 Mixin 结尾,提醒使用者它应当和具体业务类组合,而不是代表完整的“是一个”关系。
mixin 应尽量小而无状态
一个好的 mixin 通常满足:
- 只提供一个清晰能力,例如日志、序列化、权限检查;
- 不要求调用方记住隐含的初始化顺序;
- 依赖少量明确的属性或方法,并在文档中说明;
- 不拥有复杂的业务状态,也不与另一个 mixin 使用同名私有字段;
- 可以通过
super()参与协作,而不是直接操作某个具体父类。
如果类名需要写成 CacheMixinAndAuditMixinAnd...,通常说明职责切分或整体设计已经过于复杂。
空槽位 mixin
如果父类使用 __slots__,不保存实例状态的 mixin 可以声明空槽位,避免无意中引入 __dict__:
class LogMixin:
__slots__ = ()
def log(self, message: str) -> None:
print(f"[log] {message}")
class Job(LogMixin):
__slots__ = ("name",)
def __init__(self, name: str):
self.name = name
多个拥有非空 __slots__ 的父类可能发生布局冲突。需要把 __slots__ 与多重继承组合时,应让一个明确的基类负责存储字段,其余 mixin 使用 __slots__ = (),并在目标 Python 版本中运行最小示例验证。
协作式 __init__()
使用关键字参数转发
多重继承最容易出问题的地方是初始化。推荐让每个类接收自己的显式参数,并把其余参数放进 **kwargs 转发:
class Base:
def __init__(self, *, identifier: int, **kwargs):
if kwargs:
raise TypeError(f"多余参数:{', '.join(kwargs)}")
self.identifier = identifier
class LabelMixin:
def __init__(self, *, label: str, **kwargs):
self.label = label
super().__init__(**kwargs)
class ActiveMixin:
def __init__(self, *, active: bool = True, **kwargs):
self.active = active
super().__init__(**kwargs)
class Record(LabelMixin, ActiveMixin, Base):
pass
record = Record(identifier=1, label="订单", active=True)
print(record.__dict__)
调用 Record(...) 时,参数字典依次被 LabelMixin、ActiveMixin 和 Base 消费。关键字参数比位置参数更适合这种链条,因为每个类可以通过名称识别自己的参数,也不容易因父类顺序变化而错位。
叶子类可以提供更友好的公开签名
**kwargs 主要是内部协作机制。对外的叶子类可以写出明确签名,再转发到继承链:
class Job(LabelMixin, ActiveMixin, Base):
def __init__(self, identifier: int, label: str, active: bool = True):
super().__init__(identifier=identifier, label=label, active=active)
这样 IDE 和类型检查器能看到完整的构造参数,同时父类仍保持协作式实现。
不要让根类盲目调用 object.__init__()
如果协作链的最后一个类是自己的根类,可以在它确认没有剩余参数后结束:
class Root:
def __init__(self, **kwargs):
if kwargs:
raise TypeError(f"未处理的参数:{', '.join(kwargs)}")
# 不需要再调用 object.__init__(**kwargs)
object.__init__() 不接受任意业务关键字参数。把未消费的 kwargs 直接传给它,通常会得到难以理解的 TypeError;根类应负责给出清楚的错误信息。
方法冲突与显式选择
同名方法由 MRO 决定
如果多个父类定义同名方法,子类没有覆盖时,MRO 中靠前的实现会生效:
class CsvExporter:
def export(self) -> str:
return "csv"
class JsonExporter:
def export(self) -> str:
return "json"
class Exporter(CsvExporter, JsonExporter):
pass
print(Exporter().export()) # csv
如果这种优先级不是业务上明确的选择,应该在子类中覆盖方法,把决策写出来,而不是依赖父类列表的排列顺序:
class ExplicitExporter(CsvExporter, JsonExporter):
def export(self) -> str:
return JsonExporter.export(self)
这里使用显式父类调用,是因为代码明确表达“只选择 JSON 实现”,不需要让两个 exporter 组成协作链。不要把这种写法与需要所有父类参与的协作式方法混为一谈。
调整父类顺序可能改变行为
改变 class Exporter(CsvExporter, JsonExporter) 的顺序,会影响 MRO、属性查找和 super() 链。父类顺序属于行为的一部分,应该像修改公开 API 一样通过测试和变更说明来维护。
不兼容继承关系
MRO 不可线性化
某些继承声明要求同一个类同时排在自己之前和之后,Python 会拒绝创建它:
class A:
pass
class B(A):
pass
class C(A):
pass
# class Impossible(B, A, C):
# pass
# 可能抛出 TypeError:无法创建一致的方法解析顺序
实际设计中不要试图“记住所有非法组合”。看到 MRO 错误时,打印各父类的 __mro__,重新审查继承顺序,或把其中一项能力改成组合对象。
签名不兼容
即使 MRO 可以创建,父类方法的签名也可能不能协作:
class NeedsName:
def __init__(self, *, name: str):
self.name = name
class NeedsCode:
def __init__(self, *, code: int):
self.code = code
class Broken(NeedsName, NeedsCode):
pass
# Broken(name="A", code=1) # 第一个 __init__ 不会自动转发 code
如果不能修改父类使其支持 super() 和兼容的关键字参数,就不要强行把它们放进同一条初始化链。可以在子类中显式初始化两个组件,但要清楚记录这种非协作关系。
多重继承与抽象基类
多个抽象基类可以表达一个对象必须同时满足的契约:
from abc import ABC, abstractmethod
class Readable(ABC):
@abstractmethod
def read(self) -> str:
"""读取内容。"""
class Closeable(ABC):
@abstractmethod
def close(self) -> None:
"""释放资源。"""
class FileResource(Readable, Closeable):
def __init__(self, text: str):
self.text = text
def read(self) -> str:
return self.text
def close(self) -> None:
self.text = ""
抽象基类主要描述接口,不一定提供实现。若抽象基类还包含协作式方法,仍需说明调用者必须使用 super(),否则子类可能跳过其他契约的默认行为。
多重继承还是组合
多重继承更适合“一个对象可以安全地同时拥有多项行为”的场景,尤其是无状态或低耦合的 mixin。以下情况优先考虑组合:
- 两个类都是有完整生命周期的独立对象;
- 组件需要运行时替换或配置多个实例;
- 父类之间没有共同的初始化协议;
- 方法名或内部状态高度冲突;
- 继承层次需要不断增加,已经很难从类定义看懂 MRO。
组合示例:
class AuditLog:
def record(self, message: str) -> None:
print(f"审计:{message}")
class PermissionChecker:
def can_edit(self, user: str) -> bool:
return user == "admin"
class DocumentService:
def __init__(self, audit: AuditLog, permissions: PermissionChecker):
self.audit = audit
self.permissions = permissions
def update(self, user: str, content: str) -> None:
if not self.permissions.can_edit(user):
raise PermissionError("没有编辑权限")
self.audit.record(f"{user} 更新了文档")
self.content = content
这里的服务不是“一个审计日志”或“一个权限检查器”,所以把它们作为依赖注入比继承更符合对象关系,也更容易替换和测试。
调试多重继承
遇到行为不符合预期时,可以按以下顺序检查:
- 打印
type(value).__mro__,确认实际查找顺序; - 搜索 MRO 中每个类是否定义了同名属性或方法;
- 检查每个实现是否调用了
super(); - 检查
__init__()是否消费并转发了正确的关键字参数; - 为每个参与类写一个最小测试,验证它被调用一次且顺序正确;
- 如果仍需大量特殊分支,评估是否应该改用组合。
可以用 inspect.getattr_static() 查看类上的描述器而不触发属性访问逻辑,但大多数问题只需要 MRO 和普通调试日志即可定位。
小结
- 多重继承把多个父类放进一条由 MRO 决定的查找链;
super()表示 MRO 中的下一个实现,不等于写在括号里的某一个直接父类;- 协作式方法应使用
super()、兼容的签名和关键字参数转发; - mixin 应职责单一、依赖明确、尽量无状态;使用
__slots__时注意多继承布局冲突; - 菱形继承可以依靠协作式
super()让共同祖先只执行一次; - 方法冲突、父类顺序和初始化协议都属于设计契约,不能只凭“左边优先”猜测;
- 当对象关系不是“是一个”、父类不协作或 MRO 难以维护时,组合通常更合适。