如何在方法中设置属性值时解决mypy的“语句不可达[unreachable]”问题?

1 投票
2 回答
63 浏览
提问于 2025-04-12 23:36

问题描述

假设有一个测试

class Foo:

    def __init__(self):
        self.value: int | None = None

    def set_value(self, value: int | None):
        self.value = value


def test_foo():

    foo = Foo()
    assert foo.value is None
    foo.set_value(1)
    assert isinstance(foo.value, int)
    assert foo.value == 1 # unreachable

这个测试:

  • 首先,检查 foo.value 是否有值
  • 然后,使用一个方法来设置这个值。
  • 接着,检查 foo.value 是否已经改变。

在运行这个测试时,使用的是 mypy 版本 1.9.0(写这段内容时的最新版本),并且将 warn_unreachable 设置为 True,这时会得到:

(venv) niko@niko-ubuntu-home:~/code/myproj$ python -m mypy tests/test_foo.py 
tests/test_foo.py:16: error: Statement is unreachable  [unreachable]
Found 1 error in 1 file (checked 1 source file)

我发现的情况

from safe_assert import safe_assert

def test_foo():

    foo = Foo()
    safe_assert(foo.value is None)
    foo.set_value(1)
    safe_assert(isinstance(foo.value, int))
    assert foo.value == 1

后,问题依然存在(safe-assert 0.4.0)。这次,mypy 和 VS Code 的 Pylance 都认为两行之前的 foo.set_value(1) 是不可达的。

问题

我该如何告诉 mypy,foo.value 已经变成了 int,并且它应该继续检查 assert isinstance(foo.value, int) 这一行下面的所有内容呢?

2 个回答

0

在写这个问题的时候,我玩了一下 safe_assert,发现它的 0.4.0 版本在这里 使用了 NoReturn 作为返回类型。如果把这个去掉,mypy 就会很开心。所以实际上,可以这样使用 safe_assert

def safe_assert(
    expression: bool,
    message: Optional[str] = None,
):
    if not expression:
        if message:
            raise AssertionError(message)
        raise AssertionError
1

你可以通过一种叫做 TypeGuard 的特殊形式来明确控制类型的缩小(PEP 647)。通常情况下,你会用 TypeGuard 来进一步缩小已经推断出的类型,但你也可以用它来“缩小”到你选择的任何类型,即使这个类型和类型检查器已经推断出的类型不同或者更宽泛。

在这个例子中,我们将写一个函数 _value_is_set,它的返回类型标注为 TypeGuard[int],这样像 mypy 这样的类型检查器就会在调用这个函数时推断出值的类型是 int(例如,在 if 表达式中的 assert)。

from typing import TypeGuard, Any

# ...

def _value_is_set(value: Any) -> TypeGuard[int]:
    if isinstance(value, int):
        return True
    return False

def test_foo():
    foo = Foo()
    assert foo.value is None
    foo.set_value(1)
    assert _value_is_set(foo.value)
    # the next line is redundant now, but can be kept without issue
    assert isinstnace(foo.value, int) 
    assert foo.value == 1 # now reachable, according to mypy

通常情况下,mypy 应该会把 assert isinstance(...)if isinstance(...) 视为类似的处理。但出于某种原因,它在这个情况下并没有这样做。通过使用 TypeGuard,我们可以强迫类型检查器做出正确的判断。

应用这个改变后,mypy 将不会认为这段代码是不可达的

撰写回答