内联类型注释与存根导致不同的mypy行为



我的项目依赖于另一个将类型注释存储在存根文件中的项目。在.py文件中,另一个项目定义了一个基类,我需要从下面的继承这个基类

# within a .py file
class Foo:
def bar(self, *baz):
raise NotImplementedError

在相应的.ppy存根中,他们将其注释如下:

# 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会导致以下错误

error: Signature of "bar" incompatible with supertype "Foo"

如果我删除内联注释并将其添加到.ppy存根

# within a .pyi file
class SubFoo(Foo):
bar: Callable[[float], str]

mypy运行良好。

我认为这两种方法是等效的,但显然不是这样。有人能向我解释一下它们的区别吗?我需要改变什么才能使用内联注释?


@Michael0x2a回答的评论中清楚地表明,只有当您确实使用.py和.py文件时,错误才是可再现的。你可以在这里下载上面的例子。

作为一个警告,我不清楚您的代码到底是什么样子的。您定义了几个不同版本的Foo,我不确定您正试图将哪一个子类化——您的问题缺少一个可重复的最小示例。

但我猜你想做这样的事?

class Foo:
def bar(self, *baz: float) -> str:
raise NotImplementedError
class SubFoo(Foo):
def bar(self, baz: float) -> str:
pass

如果是这样的话,问题是根据基类的签名,这样做是合法的,因为Foo.bar(...)被定义为接受可变数量的参数。

f = Foo()
f.bar(1, 2, 3, 4, 5, 6, 7, 8)

但是,如果我们尝试使用您的子类来代替Foo,这段代码将失败,因为它只接受一个参数。

子类应该始终能够取代父类,而不会导致类型错误,也不会违反代码中现有的先决条件和后决条件,这种想法被称为Liskov替换原则。

但在这种情况下,为什么要进行以下类型检查?

class Foo:
bar: Callable[..., str]
class SubFoo(Foo):
def bar(self, baz: float) -> str:
pass

这是因为由于父类型的签名是Callable[..., str],mypy实际上最终会跳过对函数参数的检查。...基本上是在说"请不要麻烦对任何与我的参数相关的内容进行类型检查"。

这有点类似于使用Any类型可以将动态类型与静态类型混合使用。类似地,Callable[..., str]允许您使用动态/未确定签名来表达可调用项。

与以下程序进行对比:

class Foo:
def bar(self, *args: Any, **kwargs: Any) -> str:
pass
class SubFoo(Foo):
def bar(self, baz: float) -> str:
pass

与上一个程序不同,这个程序执行而不是类型检查——虽然Foo.bar仍然可以接受任何参数,但在这种情况下,参数的"结构"是不灵活的,mypy现在将坚持您的子类也必须能够接受任意数量的参数。


最后需要注意的是,这些行为与类型提示是否在存根中定义无关。相反,这一切都归结为函数的实际类型。

最新更新