我的项目依赖于另一个在存根文件中存储类型注释的项目。在.py文件中,另一个项目定义了一个我需要从以下继承的基类]
# within a .py file
class Foo:
def bar(self, *baz):
raise NotImplementedError
在相应的.pyi存根中,它们按如下方式注释它:
# whitin a .pyi file
from typing import Generic, TypeVar, Callable
T_co = TypeVar("T_co", covariant=True)
class Foo(Generic[T_co]):
bar: Callable[..., T_co]
对于我的项目,我想在.py文件中进行内联类型注释,并尝试在Foo
的子类中进行如下操作:
# within a .py file
class SubFoo(Foo):
def bar(self, baz: float) -> str:
pass
对此运行mypy
会导致以下错误
mypy
如果删除我的内联注释并将其添加到.pyi存根中
error: Signature of "bar" incompatible with supertype "Foo"
[# within a .pyi file
class SubFoo(Foo):
bar: Callable[[float], str]
运行正常。
我认为这两种方法是等效的,但显然并非如此。有人可以向我解释它们的不同之处,以及需要进行哪些更改才能使用内联注释来实现此目的?
作为警告,我不清楚您的代码到底是什么样子。您定义了mypy
的几个不同版本,但我不确定要尝试继承哪个版本–您的问题缺少Foo
。
但是我猜您正在尝试做这样的事情?
minimum reproducible example
如果是这样,问题是根据基类的签名,这样做是合法的,因为class Foo:
def bar(self, *baz: float) -> str:
raise NotImplementedError
class SubFoo(Foo):
def bar(self, baz: float) -> str:
pass
被定义为接受可变数量的参数。
Foo.bar(...)
但是,如果我们尝试使用您的子类代替Foo,则此代码将失败,因为它仅接受一个参数。
这种认为子类应该始终能够代替父类而不引起类型错误并且不违反代码的现有前提条件和后置条件的想法被称为f = Foo()
f.bar(1, 2, 3, 4, 5, 6, 7, 8)
。
但是在那种情况下,为什么要进行以下类型检查?
Liskov substitution principle
这是因为由于父类型的签名是class Foo:
bar: Callable[..., str]
class SubFoo(Foo):
def bar(self, baz: float) -> str:
pass
,因此mypy实际上最终完全跳过了对函数参数的检查。 Callable[..., str]
基本上是在说“请不要打扰检查与我的参数有关的任何内容”。
有点类似于使用...
类型使动态类型与静态类型混合使用。同样,Any
使您可以使用动态/不确定的签名来表示可调用项。
与以下程序进行对比:
Callable[..., str]
[与上一个程序不同,此程序执行not类型检查-尽管class Foo:
def bar(self, *args: Any, **kwargs: Any) -> str:
pass
class SubFoo(Foo):
def bar(self, baz: float) -> str:
pass
仍可以接受任何参数,但这种情况下参数的“结构”不灵活,mypy现在会坚持要求您子类还必须能够接受任意数量的参数。
最后,重要的一点是,此行为与是否在存根中定义了类型提示无关。相反,这全都取决于您的函数的实际类型。