不违反Liskov替换原则的Python模型类设计替代方案
问题描述
现有如下Python类结构定义:
from abc import ABC, abstractmethod class ModelSettings(ABC): ... class BlueModelSettings(ModelSettings): ... class RedModelSettings(ModelSettings): ... class Model(ABC): @abstractmethod def compute(self, settings: ModelSettings): ... class BlueModel(Model): def compute(self, settings: BlueModelSettings): ... class RedModel(Model): def compute(self, settings: RedModelSettings): ...
对上述代码运行MyPy静态检查时,会提示BlueModel、RedModel的compute方法实现违反里氏替换原则(LSP)。里氏替换原则要求子类方法的入参类型不能窄于基类约定的入参范围,直接在子类方法里把参数类型收紧为具体配置子类的写法,天然和该规则冲突。
需要实现满足以下要求的方案:
- 基类
Model通过类型契约明确约束:compute方法必须接收ModelSettings类型的参数,禁止传入无关类型对象 - 每个具体模型子类的
compute方法,仅允许接收和自身模型匹配的对应配置参数 - 额外约束:
Model属于公共库层面的代码,无法预先感知客户端实现的BlueModelSettings这类自定义配置类型
实现方案
采用泛型绑定抽象基类的模式即可满足所有要求,且能通过MyPy严格模式检查,不会触发LSP报错:
- 公共库层定义
Model时,将其声明为绑定ModelSettings类型的泛型类,抽象方法compute的参数用泛型参数指代,全程不需要感知任何客户端自定义类型 - 客户端实现具体模型类时,继承
Model时传入和该模型匹配的配置类作为泛型实参,MyPy会自动约束compute方法的入参类型为对应配置子类。
公共库层代码实现:
from abc import ABC, abstractmethod from typing import Generic, TypeVar # 配置基类 class ModelSettings(ABC): ... # 声明泛型参数,限定只能传入ModelSettings的子类 S = TypeVar("S", bound=ModelSettings) # 模型基类,支持泛型 class Model(ABC, Generic[S]): @abstractmethod def compute(self, settings: S): ...
客户端侧具体实现:
# 客户端自定义配置类,公共库不需要提前感知这些类型 class BlueModelSettings(ModelSettings): ... class RedModelSettings(ModelSettings): ... # 具体模型类,传入对应配置类型作为泛型参数 class BlueModel(Model[BlueModelSettings]): def compute(self, settings: BlueModelSettings): # 业务逻辑实现 ... class RedModel(Model[RedModelSettings]): def compute(self, settings: RedModelSettings): # 业务逻辑实现 ...
检查效果
- 基类契约生效:所有子类的
compute入参必须是ModelSettings的子类,传入数字、字符串、其他无关自定义对象时,MyPy会直接报类型错误 - 子类入参约束生效:调用
BlueModel().compute()时如果传入RedModelSettings或其他不匹配的配置实例,MyPy会直接提示类型不匹配 - 满足公共库扩展要求:库层代码没有引用任何客户端自定义类型,客户端后续可以任意新增模型、配置配对,不需要修改库层代码
- 无LSP报错:子类方法签名完全符合泛型基类的类型约定,MyPy检查时不会触发里氏替换原则相关错误
内容的提问来源于stack exchange,提问作者Jivan
相关产品推荐
相关产品推荐

