如何为子类中的工厂方法添加类型提示?
子类工厂模式的类型提示与mypy类方法重写报错问题
问题描述
在常见的子类工厂模式中,该如何正确为子类工厂添加类型提示?另外有个疑问:里氏替换原则原本仅适用于实例而非类,为什么mypy(1.8.0版本)会限制子类中类方法的类型标注?
代码示例
from typing import Any, Self class A: def __init__(self, a: int): self.a = a @classmethod def create(cls, a: int, **keywords: Any) -> Self: return cls(a) class B(A): def __init__(self, a: int, b: int): super().__init__(a) self.b = b @classmethod def create(cls, a: int, b: int, **keywords: Any) -> Self: return cls(a, b)
mypy报错信息
test_factory.py:19: error: Signature of "create" incompatible with supertype "A" [override] test_factory.py:19: note: Superclass: test_factory.py:19: note: @classmethod test_factory.py:19: note: def create(cls, a: int, **keywords: Any) -> B test_factory.py:19: note: Subclass: test_factory.py:19: note: @classmethod test_factory.py:19: note: def create(cls, a: int, b: int, **keywords: Any) -> B Found 1 error in 1 file (checked 1 source file)
问题解答
为什么mypy会报错?
mypy的检查逻辑是:类方法重写同样需要保证可替换性。假设你有一段代码接受Type[A]类型的参数,调用它的create(a=1)方法,如果传入的是B类,这个调用会因为缺少必填参数b直接崩溃。里氏替换原则虽然最初针对实例,但类作为可调用对象,其方法的签名兼容性同样会影响代码可靠性——mypy正是为了避免这种静态类型下的潜在错误,才会限制子类类方法的签名。
正确的类型提示方案
方案1:将子类新增参数改为强制关键字参数
把b放在*之后,强制调用时必须以关键字参数传递,既满足子类的必填要求,又不破坏父类的调用签名:
from typing import Any, Self class A: def __init__(self, a: int): self.a = a @classmethod def create(cls, a: int, **keywords: Any) -> Self: return cls(a) class B(A): def __init__(self, a: int, b: int): super().__init__(a) self.b = b @classmethod def create(cls, a: int, *, b: int, **keywords: Any) -> Self: return cls(a, b)
这样,调用B.create(1, b=2)是合法的,而依赖父类create签名的代码(比如只传a的调用)会在静态检查阶段就提示缺少b参数,避免运行时错误。
方案2:用TypeVar明确子类的类型扩展
通过绑定父类的TypeVar,让mypy理解子类的create方法是对父类签名的合理扩展:
from typing import Any, TypeVar, Self T = TypeVar('T', bound='A') class A: def __init__(self, a: int): self.a = a @classmethod def create(cls: type[T], a: int, **keywords: Any) -> T: return cls(a, **keywords) class B(A): def __init__(self, a: int, b: int): super().__init__(a) self.b = b @classmethod def create(cls: type[T], a: int, b: int, **keywords: Any) -> T: return cls(a, b, **keywords)
这个方案更灵活,允许子类扩展位置参数,同时保证返回类型的正确性。
方案3:跳过检查(不推荐)
如果确定不需要父类与子类的类方法兼容,可以用type: ignore跳过mypy的检查,但这会牺牲类型安全性:
class B(A): def __init__(self, a: int, b: int): super().__init__(a) self.b = b @classmethod def create(cls, a: int, b: int, **keywords: Any) -> Self: # type: ignore[override] return cls(a, b)
内容的提问来源于stack exchange,提问作者pchlyx
相关产品推荐
相关产品推荐

