跳到主要内容

多重继承

多重继承允许一个类同时继承多个父类: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

这里的服务不是“一个审计日志”或“一个权限检查器”,所以把它们作为依赖注入比继承更符合对象关系,也更容易替换和测试。

调试多重继承​

遇到行为不符合预期时,可以按以下顺序检查:

  1. 打印 type(value).__mro__,确认实际查找顺序;
  2. 搜索 MRO 中每个类是否定义了同名属性或方法;
  3. 检查每个实现是否调用了 super();
  4. 检查 __init__() 是否消费并转发了正确的关键字参数;
  5. 为每个参与类写一个最小测试,验证它被调用一次且顺序正确;
  6. 如果仍需大量特殊分支,评估是否应该改用组合。

可以用 inspect.getattr_static() 查看类上的描述器而不触发属性访问逻辑,但大多数问题只需要 MRO 和普通调试日志即可定位。

小结​

  • 多重继承把多个父类放进一条由 MRO 决定的查找链;
  • super() 表示 MRO 中的下一个实现,不等于写在括号里的某一个直接父类;
  • 协作式方法应使用 super()、兼容的签名和关键字参数转发;
  • mixin 应职责单一、依赖明确、尽量无状态;使用 __slots__ 时注意多继承布局冲突;
  • 菱形继承可以依靠协作式 super() 让共同祖先只执行一次;
  • 方法冲突、父类顺序和初始化协议都属于设计契约,不能只凭“左边优先”猜测;
  • 当对象关系不是“是一个”、父类不协作或 MRO 难以维护时,组合通常更合适。