该用 composition 而非 inheritance 当子类仅需复用部分逻辑却被迫继承无关行为时;优先用于功能可拆解、组合多变、“拥有”而非“是”的场景,而 inheritance 适用于明确 is-a 关系、框架强制或多态稳定需求。

什么时候该用 composition 而不是 inheritance
当你发现子类只为了复用某块逻辑,却被迫继承一整套无关的属性、方法或生命周期行为时,composition 就该上场了。比如写一个 FileProcessor 类,它需要“能重试”“能记录日志”“能校验格式”,但这些能力各自独立、可插拔——硬套继承链(RetryableFileProcessor → LoggableRetryableFileProcessor)只会让类爆炸。
常见错误现象:isinstance(obj, BaseClass) 返回 True,但你根本不需要它的类型语义,只是想调用其中两个方法;或者修改父类一个私有方法,下游十几个子类突然行为异常。
- 优先选
composition的场景:功能可拆解、组合方式多变(如策略切换)、依赖关系松散(比如“拥有一个”而不是“是一种”) - 仍适合
inheritance的场景:明确的 is-a 关系(Dog是Animal)、框架强制要求(如 Django 的View子类)、需多态分发且接口稳定 -
inheritance会隐式传递__init__、__dict__、MRO链,而composition的初始化和属性访问完全由你控制,更易测、更易 debug
__getattr__ 和 __getattribute__ 哪个更适合代理委托
想让容器类透明暴露被组合对象的方法?别急着重写一堆 def method(self, *a, **kw): return self._wrapped.method(*a, **kw) —— 用 __getattr__ 更轻量、更安全。
为什么不用 __getattribute__:它拦截所有属性访问(包括 self.__dict__),一不小心就递归崩溃;而 __getattr__ 只在常规查找失败后触发,天然适配“找不到才委托”的语义。
立即学习“Python免费学习笔记(深入)”;
- 典型误用:
__getattribute__里直接访问self._wrapped,导致无限递归(因为访问self._wrapped又触发__getattribute__) - 正确姿势:在
__getattr__中显式检查hasattr(self._wrapped, name),再返回getattr(self._wrapped, name),避免把私有属性(如_cache)也透传出去 - 性能影响:每次未命中的属性访问都会走一次
__getattr__,高频调用的属性(如循环里的item.id)建议提前绑定到实例上,或改用显式方法封装
用 typing.Protocol 替代抽象基类做组合契约
组合对象之间需要约定“你能做什么”,但又不想绑死继承关系?typing.Protocol 比 abc.ABC 更贴合组合场景——它只关心结构,不关心类型来源。
比如定义一个 Storable 协议,只要对象有 save() 和 load() 方法就能被 BackupService 接受,不管它是从 Database、FileSystem 还是 mock 出来的。
- 兼容性注意:
Protocol在运行时不检查(仅静态类型检查器如 mypy 生效),若需运行时断言,得加isinstance(obj, Storable)+ 手动hasattr判断 - 参数差异:
Protocol不支持@abstractmethod的运行时强制,也不参与MRO,所以不会干扰组合对象自身的继承体系 - 容易踩的坑:协议里写了
def close(self) -> None:,但实际对象用的是def shutdown(self) -> None:—— 名字不一致就失效,协议不是模糊匹配
嵌套组合时如何避免 self._inner._inner._inner 链式调用
三层以上组合(A 包 B,B 包 C)会让调用变得丑陋又脆弱:a.b.c.do_something() 一旦中间环节变更,所有调用点都得改。这时候得靠“扁平化委托”或“上下文感知”来收口。
核心思路:容器类不该暴露内部层级,而是把最终能力封装成自己的方法,哪怕只是转发。
- 推荐做法:在顶层类中定义
def do_something(self): return self._b.do_something(),即使_b再包_c,这个方法内部可以自己处理跳转,外部无感 - 反模式:
property直接返回self._b._c,等于把内部结构白送出去,下次重构必崩 - 调试提示:打印
obj.__class__.__mro__看继承链没用,组合对象的调用栈是纯函数调用链,出错时堆栈里不会出现“组合路径”,得靠日志打点或breakpoint()逐层进
组合真正的复杂点不在写法,而在边界划分——哪个类该负责重试,哪个该管超时,哪个只做数据转换。这些决策一旦模糊,再多的委托技巧也救不了混乱的职责分配。







