Python严格类型检查下子类作方法参数触发类型不匹配报错
问题描述
以下代码在Python开启严格类型检查时会触发类型不匹配错误:
from abc import ABC, abstractmethod class DataClass1(ABC): @abstractmethod def to_int(self) -> int: return 1 class DataClass2(DataClass1): def __init__(self, value: int): self.value = value def to_int(self) -> int: return self.value class WidgetClass(ABC): @abstractmethod def calculate(self, data: DataClass1) -> DataClass1: pass class WidgetClassImp(WidgetClass): def calculate(self, data: DataClass2): return data
触发报错的场景为:WidgetClassImp的calculate方法接收DataClass1的子类DataClass2作为入参时,类型检查器报类型不匹配。直观预期是DataClass2作为DataClass1的子类、且完整实现了DataClass1定义的所有接口,不应该触发报错,因此疑问是当前代码写法是否存在问题。
问题解答
当前代码写法确实存在问题,类型检查器报错是符合规则的,核心原因是违反了里氏替换原则的类型约束:
- 抽象基类
WidgetClass定义的接口契约非常明确:所有实现WidgetClass的类,其calculate方法必须能接收任意DataClass1类型的实例(包括所有继承自DataClass1的子类)作为入参。 WidgetClassImp将入参类型收窄为DataClass2,相当于给方法增加了父类契约里不存在的限制:它只能处理DataClass2类型的对象。如果调用方按照父类的约定,传入另一个同样实现了DataClass1接口的DataClass3实例,代码运行时很可能抛出异常,类型检查器正是提前发现了这个接口不一致的隐患。- 这里要区分类型系统里的协变、逆变规则:方法返回值支持协变,即子类方法返回父类返回值类型的子类是合法的,因为返回的对象完全符合父类要求的接口;但方法入参支持逆变,即子类方法的入参类型需要比父类定义的更宽泛,而不是更窄,你的写法刚好把入参的类型范围缩小了,自然不符合规则。
修正方式
如果业务逻辑确实要求WidgetClassImp只处理DataClass2类型的数据,可以将WidgetClass定义为泛型基类,明确不同实现对应处理的具体数据类型,既符合类型检查要求,也不会破坏接口约定:
from abc import ABC, abstractmethod from typing import Generic, TypeVar T = TypeVar("T", bound=DataClass1) class DataClass1(ABC): @abstractmethod def to_int(self) -> int: return 1 class DataClass2(DataClass1): def __init__(self, value: int): self.value = value def to_int(self) -> int: return self.value class WidgetClass(ABC, Generic[T]): @abstractmethod def calculate(self, data: T) -> T: pass class WidgetClassImp(WidgetClass[DataClass2]): def calculate(self, data: DataClass2) -> DataClass2: return data
如果WidgetClassImp理论上需要处理所有DataClass1的子类,只是内部逻辑需要用到DataClass2独有的属性,直接将入参标注为DataClass1即可,在方法内部通过isinstance做类型 narrowing 后再访问子类属性,不要直接修改方法签名的入参类型。
内容的提问来源于stack exchange,提问作者Kurian Kattukaren
相关产品推荐
相关产品推荐

