【问题标题】:Inline type annotations vs. stub results in different mypy behaviour内联类型注释与存根导致不同的 mypy 行为
【发布时间】:2020-03-20 15:02:54
【问题描述】:

我的项目依赖于将类型注释存储在存根文件中的另一个项目。在.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 会导致以下错误

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

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

# within a .pyi file

class SubFoo(Foo):
    bar: Callable[[float], str]

mypy 运行良好。

我认为这两种方法是等效的,但显然情况并非如此。有人可以向我解释它们的不同之处以及我需要更改哪些内容才能使用内联注释吗?


在@Michael0x2a 答案的 cmets 中很明显,只有当您确实使用 .py 和 .pyi 文件时,该错误才会重现。您可以从上面here下载示例。

【问题讨论】:

    标签: python python-3.6 mypy python-typing


    【解决方案1】:

    作为警告,我不清楚您的代码到底是什么样的。您定义了几个不同版本的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(...) 被定义为接受可变数量的参数。

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

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

    子类应该始终能够取代父类而不导致类型错误并且不违反代码的现有前置条件和后置条件的想法被称为Liskov substitution principle

    但是在那种情况下,为什么要做下面的类型检查呢?

    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 将现在坚持你的子类也必须能够接受任意数量的参数。


    作为最后一点,重要的是要注意,这些行为与您的类型提示是否在存根中定义无关。相反,这一切都取决于您的函数的实际类型。

    【讨论】:

    • 1.也许我说得不够清楚。 FooSubFoo 的第一个块在 .py 文件中定义,而第二个块在 .pyi 文件中定义。我将改变我的问题以更好地反映这一点。 2. 我仍在尝试处理您的答案,但据我所知,您没有回答我将如何使用Callable[..., Any] 内联注释方法。您可以将此添加到您的答案中吗?
    • @Fugu_Fish -- 将函数注释为Callable[..., Any] 内联,只需执行bar: Callable[..., Any]。从 Python 3.7 开始,这是 Python 文件中的有效语法,尽管您最终不得不在事后为 bar 分配一个函数,这很不方便。但是,无法以正常方式定义函数并为其赋予与Callable[..., Any] 完全相同的签名。最接近的选择是def bar(self, *args: Any, **kwargs: Any) -> Any
    • 另外,顺便说一句,我仍然无法重现您的错误消息。也许您可以尝试使用mypy playground 提出一个示例?我首先将你的 pyi 和 py 文件的相关部分复制并粘贴到 Playground 中——对于 Python 3,两种文件类型都使用相同的语法和类型检查规则,所以你应该能够提炼你的代码到单个文件。
    • 1.我正在使用 Python 3.6。抱歉,我不知道这会有所作为。我会把它添加到问题中。 2.确实,我也无法在操场上重现错误(或者在本地单个文件中)。似乎需要拆分 .py 和 .pyi 文件才能重现。我将尝试将这些文件作为示例添加到我的问题中。
    猜你喜欢
    • 1970-01-01
    • 2020-01-24
    • 2017-08-14
    • 2017-05-03
    • 1970-01-01
    • 2020-01-06
    • 2020-02-24
    • 2023-03-10
    • 1970-01-01
    相关资源
    最近更新 更多