派生类调用基类相同重载签名时的MyPy类型检查问题
MyPy配置
使用MyPy 1.2.0,配置如下:
[tool.mypy] show_column_numbers = true disallow_any_expr = true disallow_untyped_defs = true
问题场景
代码中存在类属性类型与另一属性值关联的逻辑:当null为True时,enum允许包含None;当null为False时,enum只能是纯字符串列表或None。相关代码如下:
from typing import List, Literal, overload class Base: null: bool enum: List[str | None] | List[str] | None @overload def __init__( self: "Base", null: Literal[True] = ..., enum: List[str | None] | None = ..., ) -> None: pass @overload def __init__( self: "Base", null: Literal[False] = ..., enum: List[str] | None = ..., ) -> None: pass def __init__( self: "Base", null: bool = False, enum: List[str | None] | List[str] | None = None, ) -> None: self.null = null self.enum = enum class Derived(Base): @overload def __init__( self: "Derived", null: Literal[True] = ..., enum: List[str | None] | None = ..., ) -> None: pass @overload def __init__( self: "Derived", null: Literal[False] = ..., enum: List[str] | None = ..., ) -> None: pass def __init__( self: "Derived", null: bool = False, enum: List[str | None] | List[str] | None = None, ) -> None: super().__init__(null=null, enum=enum)
MyPy错误信息
执行类型检查时,MyPy抛出如下错误:
overload-with-types.py:43:9: error: No overload variant of "init" of "Base" matches argument types "bool", "Union[List[Optional[str]], List[str], None]" [call-overload]
overload-with-types.py:43:9: note: Possible overload variants:
overload-with-types.py:43:9: note: def init(self, null: Literal[True] = ..., enum: Optional[List[Optional[str]]] = ...) -> None
overload-with-types.py:43:9: note: def init(self, null: Literal[False] = ..., enum: Optional[List[str]] = ...) -> None
当前修复方案
可以通过分支判断结合cast修复错误,但这种方式会随着关联属性数量增加急剧膨胀(n组二元选择会产生2ⁿ个分支):
def __init__( self: "Derived", null: bool = False, enum: List[str | None] | List[str] | None = None, ) -> None: if null: super().__init__(null=null, enum=cast(List[str | None] | None, enum)) else: super().__init__(null=null, enum=cast(List[str] | None, enum))
诉求
希望找到Pythonic的解决方案,既能保证类型检查严格性(不放宽MyPy配置),又无需编写大量分支和cast,同时让类型检查器识别基类与派生类的重载签名一致性。
解决方案
方案1:直接继承基类构造函数(优先推荐)
如果派生类不需要新增构造逻辑或参数,完全可以省略派生类的__init__定义,直接继承基类的所有重载和实现。这种方式最简洁,也不会触发类型检查错误:
from typing import List, Literal, overload class Base: null: bool enum: List[str | None] | List[str] | None @overload def __init__( self: "Base", null: Literal[True] = ..., enum: List[str | None] | None = ..., ) -> None: pass @overload def __init__( self: "Base", null: Literal[False] = ..., enum: List[str] | None = ..., ) -> None: pass def __init__( self: "Base", null: bool = False, enum: List[str | None] | List[str] | None = None, ) -> None: self.null = null self.enum = enum class Derived(Base): # 无需重新定义__init__,自动继承基类的构造逻辑和类型注解 pass
方案2:单次cast避免分支
如果派生类需要自定义__init__逻辑,可以通过一次cast将参数转换为基类重载接受的联合类型,无需分支判断。这种方式利用cast告知类型检查器参数符合重载要求,同时保持代码简洁:
from typing import List, Literal, overload, cast, Union class Base: null: bool enum: List[str | None] | List[str] | None @overload def __init__( self: "Base", null: Literal[True] = ..., enum: List[str | None] | None = ..., ) -> None: pass @overload def __init__( self: "Base", null: Literal[False] = ..., enum: List[str] | None = ..., ) -> None: pass def __init__( self: "Base", null: bool = False, enum: List[str | None] | List[str] | None = None, ) -> None: self.null = null self.enum = enum class Derived(Base): @overload def __init__( self: "Derived", null: Literal[True] = ..., enum: List[str | None] | None = ..., ) -> None: pass @overload def __init__( self: "Derived", null: Literal[False] = ..., enum: List[str] | None = ..., ) -> None: pass def __init__( self: "Derived", null: bool = False, enum: List[str | None] | List[str] | None = None, ) -> None: # 单次cast告知类型检查器参数符合基类重载要求 super().__init__( null=null, enum=cast(Union[List[str | None] | None, List[str] | None], enum) )
方案3:泛型关联属性类型(适用于复杂关联场景)
通过Generic和TypeVar绑定关联属性的类型关系,让类型检查器自动推导属性间的约束,从而避免重复重载。注意该方案需要MyPy 1.3+版本支持:
from typing import List, Literal, Generic, TypeVar # 定义绑定bool的TypeVar NullType = TypeVar('NullType', bound=bool) class Base(Generic[NullType]): null: NullType # 根据NullType的类型约束enum的类型 enum: List[str | None] | None if NullType is Literal[True] else List[str] | None def __init__( self: "Base[NullType]", null: NullType = False, enum: List[str | None] | None if NullType is Literal[True] else List[str] | None = None, ) -> None: self.null = null self.enum = enum class Derived(Base[NullType], Generic[NullType]): def __init__( self: "Derived[NullType]", null: NullType = False, enum: List[str | None] | None if NullType is Literal[True] else List[str] | None = None, ) -> None: super().__init__(null=null, enum=enum)
内容的提问来源于stack exchange,提问作者MT0

