当我有两种可能的情况时,短路“或”的狭义打字

问题描述 投票:0回答:2

我正在尝试注释以下代码。

该函数设计为在定义了

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))

有什么好的办法缩小类型吗?

python mypy python-typing
2个回答
3
投票

在代码中,当您执行 - 正如您所说的 - 非常清晰的断言时,您只能在

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
不应该再抱怨这一点。


3
投票

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
默认值,因此按照我的建议调用它已经是可能的。尽管如此,我认为仅强制执行关键字会更加明确,并且“显式优于隐式”

© www.soinside.com 2019 - 2024. All rights reserved.