接受多种类型之一的(同类)序列的函数的类型提示

Type hints for a function that accepts a (homogeneous) sequence of one of several types

我正在尝试为接受序列的函数提供类型提示 使用两个 type 元素之一,我不知道如何让 mypy 快乐。请注意,序列是同质的,这意味着类型不能混合,要么。通常我在 "compatible" 类型时这样做,例如路径 strpathlib.Path 对象,并用 Union 注释就可以了。但是在序列的情况下,Sequence[Union[..]](或Union[Sequence[..]])似乎不起作用。这是一个最小的工作示例:

from pathlib import Path
from typing import Sequence, Dict, Union


def fn_accepts_dict(adict):
    """Function from an external module that accepts `dict`s."""
    for key, val in adict.items():
        print(f"{key}, {val}")


def vararg_test(resources: Sequence[Union[str, Dict]]):
    """My function where I want to provide type hints"""
    if isinstance(resources[0], str):
        resources2 = [{"path": Path(f)} for f in resources]
    else:
        resources2 = resources
    for d in resources2:
        fn_accepts_dict(d)

现在,使用上述定义,调用 vararg_test 这两个按预期工作:

l1 = ["foo/bar", "bar/baz"]
l2 = [{"path": Path("foo/bar")}, {"path": Path("bar/baz")}]

但是 运行 mypy 给我以下错误:

type_hints.py:14: error: Argument 1 to "Path" has incompatible type "Union[str, Dict[Any, Any]]"; expected "Union[str, _PathLike[str]]"
type_hints.py:16: error: Incompatible types in assignment (expression has type "Sequence[Union[str, Dict[Any, Any]]]", variable has type "List[Dict[str, Path]]")
Found 2 errors in 1 file (checked 1 source file)

我该如何解决这个问题?

编辑: 为了提供一些背景知识,str 是一条路径,dict 具有与该路径对应的元数据,函数 fn_accepts_dict 将元数据整理到一个元数据对象中。所以他们的逻辑流程是:str -> dict -> fn_accepts_dictdict -> fn_accepts_dict.

虽然@ShadowRanger 的建议看起来很有希望,但运气不佳。我得到以下提示的相同错误:

def vararg_test2(resources: Union[Sequence[str], Sequence[Dict]]):
    ... # same implementation as above

mypy 错误:

type_hints.py:24: error: Argument 1 to "Path" has incompatible type "Union[str, Dict[Any, Any]]"; expected "Union[str, _PathLike[str]]"
type_hints.py:26: error: Incompatible types in assignment (expression has type "Union[Sequence[str], Sequence[Dict[Any, Any]]]", variable has type "List[Dict[str, Path]]")

编辑 2:不幸的是,有了所有注释,它看起来更像 C/C++ 而不是 Python;请参阅下面我的 以获得更多 Pythonic 解决方案。

我建议不要根据参数的类型在一个函数中尝试做两件不同的事情。改为定义两个不同的函数,每个函数采用特定类型的序列。

def do_with_strings(resources: Sequence[str]):
    do_with_dicts([{"path": Path(f)} for f in resources])


def do_with_dicts(resources: Sequence[dict]):
    for d in resources:
        fn_accepts_dict(d)

对于您编写的代码,resources 的类型必须是 ShadowRanger 在评论中建议的 Union[Sequence[str],Sequence[dict]],因为您假设整个列表与第一个列表具有相同的类型元素.

如果你想保持异构类型,你需要检查每一个元素以确定它需要变成一个dict:

def vararg_test(resources: Sequence[Union[str, Dict]]):
    for f in resources:
        if isinstance(f, str):
            f = {"path": Path(f)}
        fn_accepts_dict(f)

TLDR:mypy only understands isinstance(v, tp) 关于 v。它不理解 isinstance(v<expr>, tp) 关于 v,例如v[0]: str 表示 v: List[str].

的类型

mypy 不理解 isinstance(resources[0], str)resources: Sequence[str]resources: Sequence[Dict] 之间分支。这意味着 resources = resources2 推断两者都是 完全相同的 类型,这在分配或使用 resources2.

时中断

您必须注释 resources2 以防止推断类型相等,并在分支中注释 cast 以将它们标记为此类。

def vararg_test(resources: Union[Sequence[str], Sequence[Dict]]):
    """My function where I want to provide type hints"""
    resources2: Sequence[Dict]  # fixed type to prevent inferred equality
    if isinstance(resources[0], str):  # Mypy does not recognise this branch by itself!
        # exclude second Union branch
        resources = cast(Sequence[str], resources)
        resources2 = [{"path": Path(f)} for f in resources]
    else:
        # exclude first Union branch
        resources2 = cast(Sequence[Dict], resources)
    for d in resources2:
        fn_accepts_dict(d)

正如@MisterMiyagi 的回答所指出的,问题源于 mypyisinstance 中使用表达式时无法推断类型。所以我尝试了另一种实现方式,其中 isinstance 被传递了一个名称。这还有一个好处,即 vararg_test 现在可以接受任何可迭代对象。

def vararg_test(resources: Iterable[Union[str, Dict]]):
    """My function where I want to use type hints"""
    resources2: List[Dict] = []
    for res in resources:
        if isinstance(res, str):
            res = {"path": Path(res)}
        resources2 += [res]
    for d in resources2:
        fn_accepts_dict(d)

然而,这仍然需要我们注释 resources2,但很容易将其重新表述为列表理解,我们不需要任何注释。

def vararg_test3(resources: Iterable[Union[str, Dict]]):
    """My function where I want to use type hints"""
    resources2 = [
        {"path": Path(res)} if isinstance(res, str) else res
        for res in resources
    ]
    for d in resources2:
        fn_accepts_dict(d)