资讯详情

资讯详情

Python类多重继承方式

前言绝大多数面向对象语言只支持单继承一个类只能有一个直接父类。Python 是少数原生支持多重继承的主流语言——一个类可以同时继承多个基类写成class C(A, B): ...。多重继承的强大之处在于可以把若干能力片段拼装进一个类典型用法就是 Mixin混入类。但它的代价也很明显方法到底走哪条链不能光看继承列表就想清楚一旦某个环节没有接力方法会被静默跳过排查起来相当痛苦。围绕多重继承最常见的误解是把super()当成调用父类。在单继承里这么理解勉强够用但在多重继承里super()取的是方法解析顺序MRO中的下一个类它可能与书写它的类并没有继承关系。正因为这个语义多个类才能像接力一样协作——前提是每一环都按约定把调用传下去。本文讲多重继承的语法与菱形问题、协作式super()的写法含**kwargs参数接力以及 Mixin 的定位和不该用多重继承的场合。顺序由 C3 线性化决定算法的细节不在本文展开本文只把它当作一条既定的查找序列来使用。一、语法与钻石问题多重继承的语法只是把多个基类写进括号里从左到右排列# 适用于 Python 3.8class Node:def describe(self):return Nodeclass Tagged:def describe(self):return Taggedclass Document(Node, Tagged):passprint(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.8class Entity:def __init__(self, **kwargs):self.initialized Truesuper().__init__(**kwargs)class LoggerMixin(Entity):def __init__(self, log_linesNone, **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 namesuper().__init__(**kwargs)r Record(namealice, 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取走nameLoggerMixin取走log_linesSerializerMixin取走fields到Entity时只剩空字典。Entity是菱形里的公共基类两条路径都通向它但因为 MRO 里它只出现一次、每一环又都调用了super()它只被初始化一次。如果不按这套约定写问题会以两种面貌出现。一是公共基类被初始化两次# 适用于 Python 3.8class Entity:count 0def __init__(self):Entity.count 1class 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 nameLoggerMixin.__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.8import jsonclass JsonMixin:给任意类加上 to_json 能力。def to_json(self):return json.dumps(self.__dict__, ensure_asciiFalse)class TimestampMixin:def timestamp(self):return self.created_atclass User(JsonMixin, TimestampMixin):def __init__(self, name, created_at):self.name nameself.created_at created_atu 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简单直观不能复用不相关的能力多重继承 / Mixinis-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_linesNone, **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()接力下去就能避开绝大多数陷阱。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →