该用 class A 包含 B() 而不是 class A(B) 当只需部分父类行为且需灵活替换、测试隔离或避免强耦合时;继承会绑定全部接口与生命周期,易因 B 变更导致 A 失效。

什么时候该用 class A 包含 B() 而不是 class A(B)
当子类只需要部分父类行为,且这些行为可能随上下文变化、需要替换或测试隔离时,组合更合适。继承会把 B 的全部接口和生命周期强绑定到 A,一旦 B 内部改了(比如加了新方法或修改了 __init__ 参数),A 就可能意外出错。
常见信号包括:
- 你只调用
B的 1–2 个方法,却继承了它全部 20 个方法和属性 -
A的测试需要 mockB的行为——用继承时只能 patch 类方法或绕过super().__init__,而组合直接传入 mock 实例即可 -
B是第三方类(如requests.Session或sqlite3.Connection),你无法控制其演进
__init__ 里传实例比在类里硬编码 B() 更灵活
硬编码 self.db = Database() 会让单元测试难写,也堵死了换存储后端的路。应该把依赖显式传进来,哪怕默认给一个。
class UserService:
def __init__(self, db=None):
self.db = db or Database() # 可选,默认构造
这样调用时可以自由替换:
立即学习“Python免费学习笔记(深入)”;
- 测试:
UserService(MockDB()) - 开发:
UserService(InMemoryDB()) - 生产:
UserService(PostgresDB(host="..."))
注意:不要在 __init__ 里做重操作(如连接数据库),否则组合对象初始化失败时难以定位是自身问题还是依赖问题。
用属性委托代替继承后的方法转发
继承后如果只转发几个方法(比如 def save(self, *a): return self.db.save(*a)),不如用 __getattr__ 动态代理——减少样板代码,还能统一处理未实现方法。
class Repository:
def __init__(self, backend):
self._backend = backend
def __getattr__(self, name):
return getattr(self._backend, name)
但要注意:
-
__getattr__不拦截已定义的属性或方法(如__init__、__str__),所以不会覆盖你自己的逻辑 - 如果
_backend某方法抛AttributeError,会被吞掉并转抛为Repository的 AttributeError,调试时容易误判来源 - IDE 和类型检查器(如 mypy)可能无法推导动态代理的方法,需配
typing.overload或Protocol
组合后别忘了生命周期管理
继承时,父类资源通常和子类共生死;组合则要自己管。比如 self.session = requests.Session(),你得确保它被关闭——否则连接泄漏。
- 用上下文管理器:
with UserService(db) as service:,在__enter__/__exit__中控制db.close() - 或者暴露显式清理方法:
service.close(),并在文档里强调“必须调用” - 避免在
__del__里关资源——不可靠,且可能触发循环引用
最易忽略的是:组合对象被长期持有(如全局单例或 Flask 应用中的 g 对象)时,底层依赖的连接/文件句柄是否复用合理、有没有超时重连逻辑。






