mypy: isinstance-condition 中的函数定义:为什么条件中的类型没有缩小?

mypy: function definition in isinstance-condition: why is the type in the condition not narrowed down?

我正在基于变量类型(其他类型的联合)的条件内定义函数。 mypy 推断在这种情况下类型被缩小了,但似乎抛弃了这一知识。

我不明白为什么。这是 mypy 中的错误吗?

考虑一下:

from typing import Union, Callable

def test(a: Union[None, Callable]):
    if a is None:
        pass
    else:
        def test0():
            a()
        def test1(b: str):
            a(b)
        callable_a = a
        def test2(b: str):
            callable_a(b)
        a()
        a('foo')

mypy 的输出:

test.py:9: error: "None" not callable

Found 1 error in 1 file (checked 1 source file)

第 9 行是 test1 的正文。似乎这里应用了整个 Union[None, Callable]
(“整个联合”从这个例子中看不清楚,在实际代码中它是 Union[None, Sequence, Callable] 和 mypy objects None 和 Sequence 都不可调用)。

在函数定义之外,问题没有出现。

一些想法:

我是 运行 mypy 0.950 如果它有所作为。

这是正确的行为,在 mypy docs 中有很好的解释。 a 的推断是完全错误的,因为后期绑定。

这是文档中给出的示例:

from typing import Callable, Optional

def foo(x: Optional[int]) -> Callable[[], int]:
    if x is None:
        x = 5
    print(x + 1)  # mypy correctly deduces x must be an int here
    def inner() -> int:
        return x + 1  # but (correctly) complains about this line

    x = None  # because x could later be assigned None
    return inner

inner = foo(5)
inner()  # this will raise an error when called

现在谈谈您的代码。首先,不检查没有注释的函数,因此您看不到所有真正的错误。添加类型提示后,我们有以下内容:

from typing import Union, Callable

def test(a: Union[None, Callable]):
    if a is None:
        pass
    else:
        def test0() -> None:
            a()  # E: "None" not callable
            
        def test1(b: str) -> None:
            a(b)  # E: "None" not callable
        
        callable_a = a
        reveal_type(callable_a)  # N: Revealed type is "def (*Any, **Any) -> Any"
        def test2(b: str) -> None:
            callable_a(b)
        
        a()
        a('foo')

现在只有 test2 有效,这是预期的(见显示类型)。 callable_a 被推断为 Callable 只有 is None 类型保护,所以你不能稍后分配 None 给它。您可以使用 test0test1 中的 assert callable(a)(肮脏的方式)或使用与 test2 相同的模式来解决此问题。默认参数绑定(使用 def test0(a: Callable = a) 应该也是有效的,但 mypy 出于某种原因不喜欢它 - 这是一个误报,但可能太难处理或开发人员没有时间现在。

浏览 this issue if you are interested in mypy opinion on that, and here's a playground 在线玩这个。