组合是python业务场景下唯一合理的选择,因继承存在运行时不可替换、mro失控、语义易错等问题;仅当is-a关系稳定且契约明确时才用继承。

Python项目里,组合不是“比继承好一点”,而是绝大多数业务场景下唯一合理的选择——因为继承在运行时无法替换、MRO容易失控、语义易错,而组合天然适配Python的鸭子类型和动态特性。
什么时候该用 class A(B) 而不是 self.b = B()?
只有一种情况真正需要继承:is-a 关系稳定、契约明确、且不可变。比如 int 继承 numbers.Real,或自定义异常类继承 Exception。其余多数场景——日志、缓存、网络客户端、策略算法——都该用组合。
- 继承强制共享所有父类方法,哪怕你只想要其中一两个;组合则按需持有、按需委托
- 当你发现子类只重写
__init__和一两个方法,其他全靠父类兜底,这基本是误用继承的信号 - 第三方库升级后父类加了新方法?子类可能意外继承一个同名但语义冲突的函数——组合不会发生这种事
__getattr__ 是组合的双刃剑,别拿来偷懒
用 __getattr__ 实现自动委托看似省事,但会掩盖接口契约,让 IDE 补全失效、类型检查失准、调试时找不到方法来源。
快速生成专业的 Python 脚本和应用代码。一键创建完整项目结构,支持CLI、API、爬虫、Bot、Django等多种项目类型,包含完整的项目结构、配置文件、依赖管理、测试、README和文档。
- 显式委托更安全:
def start(self): return self.engine.start() - 若组件接口统一,优先用
typing.Protocol做类型提示,比如定义class Sendable(Protocol): def send(self) -> None: ... -
__getattr__仅适合内部工具类或极简胶水层,业务主逻辑中应避免
测试时组合的优势直接体现在 mock 方式上
继承结构下 mock 父类方法要打补丁、调用 super()、处理 __init__ 顺序;组合只需换掉实例属性。
- 测试邮件服务时:
svc.transport = MockTransport()即可,不用动类定义 - 测试失败重试逻辑:
svc.retryer = NoRetry(),零侵入切换行为 - 继承方式下,mock
super().send()往往要依赖patch.object(A, 'send'),路径难写、易断
多重组合不等于过度拆分,关键看生命周期和复用意图
一个 PaymentProcessor 持有 self.gateway、self.logger、self.validator 没问题;但把 HTTP 请求头字段单独抽成 Headers 类再组合进来,就是画蛇添足。
- 判断标准:这个对象是否需要被独立替换、独立测试、跨多个上下文复用?
- 如果只是封装字典或简单计算,用普通属性(如
self._headers: dict)更直白 - 组合的粒度由“变化频率”决定,不是由“代码行数”决定
真正容易被忽略的是:组合不是靠堆砌类来实现的,而是靠清晰的职责边界和显式的依赖声明。一旦你开始为每个小功能都建一个类,却没定义清楚它该响应什么协议、该由谁负责生命周期管理,那组合就退化成了更难维护的抽象套娃。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!










