如何将 typing.Union 转换为 Python 中的其中一个子类型?

How to cast a typing.Union to one of its subtypes in Python?

我正在使用 Python 3.6.1、mypy 和输入模块。我创建了两个自定义类型,FooBar,然后在一个函数的字典中使用它们 return。该字典被描述为将 str 映射到 FooBarUnion。然后我想在一个函数中使用这个字典中的值,每个函数只命名一个参数:

from typing import Dict, Union, NewType

Foo = NewType("Foo", str)
Bar = NewType("Bar", int)

def get_data() -> Dict[str, Union[Foo, Bar]]:
    return {"foo": Foo("one"), "bar": Bar(2)}

def process(foo_value: Foo, bar_value: Bar) -> None:
    pass

d = get_data()

我尝试按原样使用这些值:

process(d["foo"], d["bar"])
# typing-union.py:15: error: Argument 1 to "process" has incompatible type "Union[Foo, Bar]"; expected "Foo"
# typing-union.py:15: error: Argument 2 to "process" has incompatible type "Union[Foo, Bar]"; expected "Bar"

或使用类型:

process(Foo(d["foo"]), Bar(d["bar"]))
# typing-union.py:20: error: Argument 1 to "Foo" has incompatible type "Union[Foo, Bar]"; expected "str"
# typing-union.py:20: error: Argument 1 to "Bar" has incompatible type "Union[Foo, Bar]"; expected "int"

如何将 Union 转换为其子类型之一?

你必须使用 cast():

process(cast(Foo, d["foo"]), cast(Bar, d["bar"]))

来自 PEP 484 的 Casts 部分:

Occasionally the type checker may need a different kind of hint: the programmer may know that an expression is of a more constrained type than a type checker may be able to infer.

无法拼写特定类型的值与字典键的特定值对应。您可能需要考虑返回一个 named tuple,可以按键输入:

from typing import Dict, Union, NewType, NamedTuple

Foo = NewType("Foo", str)
Bar = NewType("Bar", int)

class FooBarData(NamedTuple):
    foo: Foo
    bar: Bar

def get_data() -> FooBarData:
    return FooBarData(foo=Foo("one"), bar=Bar(2))

现在类型提示器确切知道每个属性类型是什么:

d = get_data()
process(d.foo, d.bar)

或者您可以使用 dataclass:

from dataclasses import dataclass

@dataclass
class FooBarData:
    foo: Foo
    bar: Bar

这使得添加可选属性以及控制其他行为(例如相等性测试或排序)变得更加容易。

比起 typing.TypedDict,我更喜欢两者之一,后者更适合与遗留代码库和 (JSON) 序列化一起使用。

虽然我认为在您的情况下使用转换可能是正确的选择,但我只是想简单地提一下可能适用于类似场景的另一个选项,以解决问题:

实际上可以使用新的实验性 TypedDict 功能更精确地键入您的字典,该功能在最新版本的 mypy 中可用(如果您从 github 存储库克隆)并且很可能是在下一个 pypi 版本中可用。

为了使用 TypedDict,您需要通过 运行 pip install mypy_extensions.

从 pypi 安装 mypy_extensions

TypedDict 允许您为字典中的每个项目分配单独的类型:

from mypy_extensions import TypedDict

Foo = NewType("Foo", str)
Bar = NewType("Bar", int)

FooBarData = TypedDict('FooBarData', {
    'foo': Foo,
    'bar': Bar,
})

您还可以在 Python 3.6+:

中使用基于 class 的语法定义 FooBarData
from mypy_extensions import TypedDict

Foo = NewType("Foo", str)
Bar = NewType("Bar", int)

class FooBarData(TypedDict):
    foo: Foo
    bar: Bar

您还提到您的字典可以包含动态数量的元素。如果它确实是动态的,那么 TypedDict 将无济于事,原因与 NamedTuple 无济于事的原因相同,但是如果您的 TypedDict 最终将具有有限数量的元素,并且您只是逐渐向其中添加项目而不是全部立即,您可以尝试使用 non-total TypedDicts, or try constructing TypeDicts that mix required and non-required items.

还值得注意的是,与几乎所有其他类型不同,TypedDicts 使用结构类型而不是名义类型进行检查。这意味着,如果您定义一个完全不相关的 TypedDict,例如 QuxData,它也有 foobar 字段,其类型与 FooBarData 相同,那么 QuxData 实际上是 FooBarData 的有效子类型。稍微聪明一点,这可能会打开一些有趣的可能性。