如何在方法中设置属性值时解决mypy的“语句不可达[unreachable]”问题?
问题描述
假设有一个测试
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)
我发现的情况
- 在 mypy 的 GitHub 上有一个未解决的问题:https://github.com/python/mypy/issues/11969 有个评论建议使用 safe-assert,但是在将测试重写为
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 个回答
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 将不会认为这段代码是不可达的。