继承仅在“子类确实是父类的一种”(is-a)时适用,如ElectricCar是Car;否则应优先用组合,因其更灵活、易测试、解耦且避免MRO等问题。

什么时候该用继承而不是组合
继承只在「子类确实是父类的一种」时才成立,比如 ElectricCar 是 Car 的一种,这时用继承语义清晰、方法复用自然。但若只是为了复用代码而强行拉出一个父类(比如把日志功能抽成 LoggerBase 让所有类去继承),就违背了 Liskov 替换原则——你不能把任意 LoggerBase 实例替换成它的子类而不破坏逻辑。
常见错误现象:TypeError: Can't instantiate abstract class 或子类重写太多父类方法导致调用链混乱;更隐蔽的问题是,父类一改,十几个子类全得跟着测。
- 判断标准:能否用「is-a」自然描述?不能,就别用继承
- 参数差异:继承会强制共享初始化签名,组合则可自由控制依赖注入方式
- 测试影响:继承关系下,单元测试常需 mock 父类行为,组合则可直接替换协作对象
组合在什么场景下更可控
组合适用于「某类需要使用另一类的能力,但不构成类型层级关系」的场景,比如 OrderProcessor 需要发邮件、查库存、记日志——这些能力分别由 EmailService、InventoryClient、Logger 提供,它们之间没有 is-a 关系,硬套继承只会让类膨胀且难以拆分。
典型误用:用多重继承模拟组合(如 class A(B, C, D)),结果方法解析顺序(MRO)出人意料,super() 调用链断裂,调试时连哪个 __init__ 先执行都搞不清。
立即学习“Python免费学习笔记(深入)”;
- 初始化更灵活:组合对象可在
__init__中传入,也可延迟创建或动态替换 - 避免菱形继承问题:Python 支持多重继承,但 MRO 一旦复杂,
super()行为极易偏离预期 - 利于 mock:测试时直接传入 stub 或 fake 对象,无需 patch 类属性或绕过
__new__
__init__ 和 super() 在继承链中的实际表现
Python 的 super() 不是指“父类”,而是按 MRO 返回下一个类。很多人以为 super().__init__() 就是调父类构造器,结果在多层继承中漏掉某个 __init__,或者在未显式调用时导致协作类未初始化(比如 Mixin 中的资源未 setup)。
示例:如果 class A(B, C),且 B 和 C 都定义了 __init__,但 B.__init__ 里没写 super().__init__(),那 C.__init__ 根本不会执行——这不是 bug,是 MRO 严格按顺序走的结果。
- 必须每层都显式调用
super().__init__(),否则链中断 - 参数传递要对齐:如果某层
__init__多收了一个timeout参数,下游super()调用就得处理或透传,否则报TypeError - 组合完全规避此问题:每个协作对象自己管自己的初始化,主类只负责组装
性能与可维护性的隐性成本
表面上看,继承调用方法更快(少一次属性查找),但真实项目里这点开销几乎不可测;反倒是继承带来的耦合会让重构举步维艰。比如要把 DatabaseHandler 换成异步版本,若它被十几处继承,每处都得改 super().query() 调用方式;而组合只需换掉注入的对象实例。
另一个常被忽略的点:IDE 和静态分析工具(如 mypy)对组合的支持更稳定。继承深度超过三层后,类型推导容易失败,self 的类型可能变成 Any,而组合对象的类型声明清晰明确。
- 继承越深,文档和类型注解越难准确表达实际行为
- 组合对象可独立演进、单独测试、甚至运行时热替换(如插件系统)
- 真正难的不是语法选择,而是识别出哪些职责本就不该塞进同一个类继承树里










