我正在尝试注释以下代码。
该函数设计为在定义了
zone
和 zones
时工作,或者在定义了 file
时(但不是同时定义了两者):
def get_file(zone: str, zones: dict[str, str]) -> pathlib.Path:
pass
def connect(
zone: str | None = None,
zones: dict[str, str] | None = None,
file: pathlib.Path | None = None,
) -> bool:
file = file or get_file(zone, zones)
但这让
mypy
生气-
1. Argument of type "str | None" cannot be assigned to parameter "zone" of type "str" in function "_get_vpn_file"
Type "str | None" cannot be assigned to type "str"
Type "None" cannot be assigned to type "str"
2. Argument of type "dict[str, str] | None" cannot be assigned to parameter "zones" of type "dict[str, str]" in function "_get_vpn_file"
Type "dict[str, str] | None" cannot be assigned to type "dict[str, str]"
Type "None" cannot be assigned to type "dict[str, str]"
然后我尝试进行一些激进的类型缩小:
def _check_params_are_ok(
zone: str | None, zones: dict[str, str] | None, file: pathlib.Path | None,
) -> tuple[str, dict[str, str], None] | tuple[None, None, pathlib.Path]:
if zone is not None and file is not None:
raise ValueError("Pass `file` or `zone`, but not both.")
if zone is not None and zones is None:
raise ValueError("connect: Must define `zones` when `zone` is defined.")
if zone is None and file is None:
raise ValueError("connect: Must define `zone` or `file`.")
assert file is not None or (zone is not None and zones is not None)
# Type narrowing
if zone is not None and zones is not None and file is None:
return zone, zones, file
if zone is None and zones is None and file is not None:
return zone, zones, file
raise NotImplementedError("This error from _check_params_ok shouldn't happen.")
def connect(
zone: str | None = None,
zones: dict[str, str] | None = None,
file: pathlib.Path | None = None,
) -> bool:
zone, zones, file = _check_params_are_ok(zone, zones, file)
file = file or get_file(zone, zones)
mypy 仍然显示相同的错误。
即使添加非常明确的断言,Mypy 仍然显示相同的错误:
zone, zones, file = _check_params_are_ok(zone, zones, file)
if file is None:
assert zone is not None and zones is not None
file = file or get_file(zone, zones)
到目前为止,我发现的最好的解决方案是将类型转换为内联,但它会影响代码的可读性并使该行难以阅读:
file = file or get_file(cast(str, zone), cast(dict[str, str], zones))
有什么好的办法缩小类型吗?
在代码中,当您执行 - 正如您所说的 - 非常清晰的断言时,您只能在
file
语句的主体中分配给if
:
if file is None:
assert zone is not None and zones is not None
file = get_file(zone, zones)
# file is now a pathlib.Path object
assert file.is_file() # this is now valid
那么你就会非常明确,
mypy
不应该再抱怨这一点。
Stefan Marinov的答案是正确的。断言对于这些情况很有用。
为了完整起见,我想补充一点,在此类构造中,当您知道应该只出现特定调用参数组合时,建议定义您的重载调用变体:
from pathlib import Path
from typing import overload
def get_file(zone: str, zones: dict[str, str]) -> Path:
return NotImplemented
@overload
def connect(zone: str, zones: dict[str, str], file: None = None) -> bool:
...
@overload
def connect(zone: None, zones: None, file: Path) -> bool:
...
def connect(
zone: str | None = None,
zones: dict[str, str] | None = None,
file: Path | None = None,
) -> bool:
if file is None:
assert zone is not None and zones is not None
file = get_file(zone, zones)
return NotImplemented
这允许类型检查器(以及扩展您的 IDE)提醒您
connect
的错误使用。例如,当正确定义重载签名时,这两个调用都会导致 mypy
错误:
connect(None, None, None)
connect("abc", {"x": "y"}, Path("."))
尽管如此,无论它的价值如何,我建议至少重新考虑一下,如果这个设计确实是你想要的。将函数调用为
connect(None, None, Path(...))
对我来说似乎非常不自然。如果这确实是函数应该被调用的方式,我可能至少会选择将所有参数设置为仅关键字,从而允许更明确和可读的调用:
from pathlib import Path
from typing import overload
def get_file(zone: str, zones: dict[str, str]) -> Path:
return NotImplemented
@overload
def connect(*, zone: str, zones: dict[str, str]) -> bool:
...
@overload
def connect(*, file: Path) -> bool:
...
def connect(
*,
zone: str | None = None,
zones: dict[str, str] | None = None,
file: Path | None = None,
) -> bool:
if file is None:
assert zone is not None and zones is not None
file = get_file(zone, zones)
return NotImplemented
connect(file=Path("."))
connect(zone="abc", zones={"x": "y"})
但这可能只是主观的。您更清楚自己需要什么来实现您的目的。
编辑:刚刚意识到该实现对所有参数都有
None
默认值,因此按照我的建议调用它已经是可能的。尽管如此,我认为仅强制执行关键字会更加明确,并且“显式优于隐式”。