多重继承
Python 支持一个类同时继承多个基类。多重继承是强大的代码复用工具,但也引入了方法解析的复杂性。理解 MRO(Method Resolution Order)算法是正确使用多重继承的前提。
多继承语法
在类定义的圆括号中列出多个基类,即可实现多重继承:
class Base1:
def method1(self):
return 'Base1'
class Base2:
def method2(self):
return 'Base2'
class Derived(Base1, Base2):
pass
d = Derived()
print(d.method1()) # Base1
print(d.method2()) # Base2
基类在圆括号中的顺序至关重要。当多个基类定义了同名方法时,Python 需要一套规则来决定调用哪一个。这套规则就是 MRO。
方法解析顺序(MRO)
Python 使用 C3 线性化算法 计算 MRO。该算法保证:
- 子类优先于父类:派生类总是出现在其基类之前
- 尊重声明顺序:基类在类头中的从左到右顺序被保留
- 单调性:一个类被继承时,不会影响其父类的优先顺序
- 每个类只出现一次:即使通过多条路径可达,类在 MRO 中仅出现一次
通过 __mro__ 属性或 mro() 方法可以查看计算结果:
class A:
pass
class B(A):
pass
class C(A):
pass
class D(B, C):
pass
print(D.__mro__)
# (<class 'D'>, <class 'B'>, <class 'C'>, <class 'A'>, <class 'object'>)
注意 B 排在 C 之前,因为 D(B, C) 的声明顺序是 B 在前。同时 A 只出现一次,尽管它同时是 B 和 C 的父类。
如果 C3 算法无法产生满足所有约束的线性顺序,Python 会抛出 TypeError:
class X:
pass
class Y:
pass
class Z(X, Y):
pass
# class W(Y, X): # 若取消注释,与 Z 冲突
# pass
# class Bad(Z, W): # TypeError: Cannot create a consistent method resolution
# pass # order (MRO) for bases X, Y
钻石问题
钻石问题(Diamond Problem)是多重继承的经典难题:当两个子类继承自同一个父类,而又有第四个子类同时继承这两个子类时,最顶层的父类通过两条路径被访问。
class Top:
def method(self):
return 'Top'
class Left(Top):
def method(self):
return 'Left'
class Right(Top):
def method(self):
return 'Right'
class Bottom(Left, Right):
pass
b = Bottom()
print(b.method()) # Left —— 按 MRO,Left 先于 Right
print(Bottom.__mro__)
# (Bottom, Left, Right, Top, object)
Python 的 C3 MRO 确保 Top 只被访问一次,且位于 Left 和 Right 之后。这避免了某些语言中父类被重复初始化的问题。
super() 的协同调用
在多重继承中,super() 的行为与单继承有本质不同。它并不总是调用直接父类,而是按照 MRO 找到下一个类。
class Top:
def __init__(self):
print('Top init')
super().__init__()
class Left(Top):
def __init__(self):
print('Left init')
super().__init__()
class Right(Top):
def __init__(self):
print('Right init')
super().__init__()
class Bottom(Left, Right):
def __init__(self):
print('Bottom init')
super().__init__()
Bottom()
# Bottom init
# Left init
# Right init
# Top init
每个 super().__init__() 都沿着 MRO 链条调用下一个类的 __init__,最终 Top 只执行一次。这种设计被称为协同多重继承(cooperative multiple inheritance),要求继承链上的每个类都使用 super() 并遵循相同的函数签名约定。
如果某个中间类不使用 super(),链条会断裂:
class BrokenLeft(Top):
def __init__(self):
print('BrokenLeft init')
# 没有调用 super()!
class Bottom2(BrokenLeft, Right):
def __init__(self):
print('Bottom2 init')
super().__init__()
Bottom2()
# Bottom2 init
# BrokenLeft init
# Right 和 Top 的 __init__ 不会被执行!
混入类
混入类(Mixin)是一种设计模式:只提供特定功能片段、不打算独立实例化的小类。混入类通常不定义 __init__,只包含若干方法,通过多重继承组合到主类中。
class JSONSerializableMixin:
"""混入类:提供 JSON 序列化能力"""
def to_json(self):
import json
return json.dumps(self.__dict__)
class ComparableMixin:
"""混入类:提供基于 __eq__ 的 != 运算符"""
def __ne__(self, other):
return not self.__eq__(other)
class Person(JSONSerializableMixin, ComparableMixin):
def __init__(self, name, age):
self.name = name
self.age = age
def __eq__(self, other):
if not isinstance(other, Person):
return NotImplemented
return self.name == other.name and self.age == other.age
p1 = Person('Alice', 30)
p2 = Person('Alice', 30)
print(p1.to_json()) # {"name": "Alice", "age": 30}
print(p1 != p2) # False
混入类的命名约定通常以 Mixin 结尾,明确表明其用途。良好的混入类设计应当:
- 不依赖主类的具体实现细节,只依赖约定接口
- 不定义实例变量,或确保变量名不会与主类冲突
- 使用
super()协同调用,以便与其他混入类共存
多重继承的设计建议
多重继承虽然强大,但过度使用会导致代码难以追踪。实践中建议:
- 优先使用组合:"有一个"关系用组合,"是一个"关系才用继承
- 限制继承深度:继承层次超过三层时,MRO 往往难以直观推断
- 协同调用约定:设计可继承的类时,始终在
__init__中使用super(),并接收**kwargs传递未消费的参数 - 抽象基类辅助:使用
abc.ABC定义接口,强制子类实现约定方法,降低混入类的耦合风险
from abc import ABC, abstractmethod
class LoggerMixin(ABC):
@abstractmethod
def log_target(self):
"""子类必须返回日志输出目标"""
pass
def log(self, message):
target = self.log_target()
target.write(f"[LOG] {message}\n")