当子类与父类存在明确is-a关系且符合liskov替换原则时才用继承;否则优先组合,通过依赖注入实现松耦合与可测试性。

什么时候该用继承而不是组合
当子类和父类之间存在明确的 IS-A 关系,且子类确实“是”父类的一种时,继承才合理。比如 Dog 是 Animal,FileReader 是 Reader——它们共享行为契约,能被同一接口安全替换(Liskov 替换原则)。
常见误用:为复用代码强行继承 BaseLogger 或 ConfigMixin,结果子类既不是 logger 也不是 config,只是“用到了日志或配置”。这时继承破坏语义,还导致子类被迫暴露不相关接口。
- 检查是否能自然说出“X 是 Y 的一种”,不能就别继承
- 若只是为了调用父类方法,优先考虑把逻辑抽成独立函数或工具类
- 继承后子类必须能完全替代父类出现在所有使用父类的地方,否则迟早出
TypeError或逻辑错乱
组合的典型写法与初始化陷阱
组合本质是“HAS-A”,比如 Car 有 Engine,OrderService 有 PaymentGateway。关键在初始化时机和依赖注入方式。
错误写法是直接在 __init__ 里硬编码实例:self.engine = Engine()。这导致测试难、替换难、生命周期耦合紧。
- 把依赖作为参数传入
__init__,允许外部控制创建方式(如 mock 或不同实现) - 避免在组合对象内部调用依赖的私有方法(如
engine._start_internal()),这会泄露实现细节 - 注意循环引用:A 组合 B,B 又组合 A —— Python 不报错但 GC 可能延迟回收,用
weakref或重构拆分职责
@property 和 __getattr__ 在组合中的边界
组合后常想“透传”依赖的方法,比如让 car.start() 实际调用 car.engine.start()。但盲目转发会模糊责任归属,也容易掩盖类型错误。
@property 适合封装状态代理(如 car.fuel_level 映射到 self.tank.level);而 __getattr__ 是兜底机制,只应在明确知道缺失属性必然属于某个组合对象时使用。
- 用
@property时,确保返回值类型稳定,不要让它动态返回不同类的实例 -
__getattr__中必须先检查目标对象是否存在该属性,否则可能无限递归(return getattr(self._engine, name)要加hasattr(self._engine, name)判断) - IDE 和类型检查器(如 mypy)对
__getattr__不友好,建议配合typing.overload或文档说明可代理的方法
从继承迁移到组合的实际步骤
已有继承结构想改成组合,别重写,用渐进式替换。核心是把原父类功能剥离为独立组件,再注入到原子类中。
例如原 class ReportGenerator(HtmlRenderer):,现在要解耦渲染逻辑。先提取 HtmlRenderer 为普通类,去掉 class HtmlRenderer: 中的 pass 父类声明,再让 ReportGenerator 持有它:
class ReportGenerator:
def __init__(self, renderer=None):
self._renderer = renderer or HtmlRenderer()
def render(self):
return self._renderer.render_html(self.data)
之后可轻松切换为 MarkdownRenderer 或加 mock 测试。
- 迁移时保留原继承接口(如仍提供
render()方法),避免下游代码批量报错 - 原父类中带状态的属性(如
self.cache)要一并移到组合对象里,不能留在子类中双份维护 - 如果原父类有
__init__参数,这些参数应转为组合对象的构造参数,而非子类的参数——否则语义混乱
组合不是银弹,它让关系更清晰,但也增加一层间接性。真正难的不是语法怎么写,而是判断哪个对象该负哪部分责——这点没法靠工具检查,得看代码跑起来时谁改一点,另一处就莫名崩了。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











