应使用 self.engine = Engine(),因为 Car 与 Engine 是 has-a 关系而非 is-a;继承易导致无关方法泄漏、类型检查误判及耦合度高,组合更安全、灵活且符合行为契约。

什么时候该用 class Car(Engine) 还是 self.engine = Engine()
答案很直接:90% 的业务场景里,self.engine = Engine() 更安全、更可控。因为 Car 并不是一种 Engine,它只是“有一个”引擎——这是典型的 has-a 关系,不是 is-a。强行用继承会让 Car 意外继承 Engine.start() 以外的无关方法(比如 Engine.calibrate_sensors()),还可能让类型检查器误判行为契约。
isinstance(obj, Parent) 会出问题的典型场景
当你写 isinstance(car, Vehicle) 来做类型判断时,如果 car 是通过组合实现的,这个判断就失效了——但这恰恰是好事。它逼你回归行为本身,而不是依赖类型身份:
- 错误做法:靠
isinstance(x, Logger)决定是否调用x.log() - 正确做法:只检查
hasattr(x, 'log') and callable(x.log),或用typing.Protocol约束接口 - 真实坑点:测试时 mock 一个
Logger类去继承BaseLogger,结果发现它被isinstance(..., BaseLogger)拦在中间逻辑外
动态替换组件比重写 __init__ 容易得多
组合天然支持运行时注入不同实现,而继承在类定义那一刻就锁死了父类行为:
- 想让
Car在测试中用MockEngine()?直接Car(engine=MockEngine()) - 想切换生产环境用
ElectricEngine()、预发用GasEngine()?改构造参数就行,不用动类结构 - 继承方案下要换行为,往往得新增子类(
ElectricCar)、再改所有调用点,耦合度飙升
协议约束比抽象基类更轻量且更 Pythonic
Python 不需要靠 abc.ABC 强制实现,用 typing.Protocol 声明最小契约即可:
安全 Python 代码沙箱:在隔离的沙箱中执行任意 Python 代码,预装 requests、NumPy、pandas 库。返回 stdout、stderr。
立即学习“Python免费学习笔记(深入)”;
from typing import Protocol
class Startable(Protocol):
def start(self) -> None: ...
class Car:
def __init__(self, engine: Startable) -> None:
self.engine = engine
这样既获得类型检查收益,又不强制继承;Engine、Motor、甚至一个带 start() 方法的 dict 子类都能传进来。而继承容易让人陷入“先建好父类再硬塞子类”的思维惯性,忽略真正要协作的是行为,不是类名。
最容易被忽略的一点是:组合不是“把所有东西都拆成类”,而是只拆那些**需要独立替换、测试或配置**的部分。一个 _headers: dict 就够用的地方,硬抽成 Headers 类反而增加间接层——判断标准永远是“这个东西会不会有多种实现?”而不是“它看起来像不像一个类?”

















