【问题标题】:Pattern matching over nested `Union` types in PythonPython中嵌套“Union”类型的模式匹配
【发布时间】:2021-04-30 21:46:48
【问题描述】:

构建一个 Python 库,我使用类型提示来保证某些数据表示的一致性。特别是,我以嵌套方式使用Union(求和类型)来表示数据可以采用的不同“风格”。

到目前为止,我得到的结果类似于以下示例:

from typing import Union

MyNumberT = Union[float,int]
MyDataT = Union[str,MyNumber]

def my_data_to_string(datum: MyDataT) -> str:
    if isinstance(datum, float):
        return _my_number_to_string(datum)
    elif isinstance(datum, int):
        return _my_number_to_string(datum)
    elif isinstance(datum, str):
        return datum
    # assert_never omitted for simplicity

def _my_number_to_string(number: MyNumberT) -> str:
    return "%s" % number

使用mypy 可以很好地进行类型检查。

现在,我的真实代码有点复杂,我需要对MyNumberT 类型的变量执行一些常见操作。 在示例中,这只是通过调整 import 并替换 my_data_to_string 来突出显示,如下所示:

from typing import get_args, Union

[...]

def my_data_to_string(datum: MyDataT) -> str:
    if isinstance(datum, get_args(MyNumberT)):
        return _my_number_to_string(datum)
    elif isinstance(datum, str):
        return datum
    # assert_never omitted for simplicity

[...]

mypy 的类型检查失败: Argument 1 to "_my_number_to_string" has incompatible type "Union[str, Union[float, int]]"; expected "Union[float, int]".

我希望 mypy 能够“意识到”在第一个分支中,datum 只能是 floatint 类型,但错误消息表明情况并非如此......

我怎样才能在这种嵌套类型的“部分”上实现一些模式匹配?

【问题讨论】:

  • 明确一点:您想在不重复其组成部分的情况下检查 datumMyNumberT
  • 是的!我想避免在这个函数中暴露子UnionMyNumberT的细节。

标签: python pattern-matching type-hinting mypy union-types


【解决方案1】:

您的用例是使用functools 提供的名为singledispatch 的实用程序的一个很好的例子。它允许您根据输入类型为单个函数定义多个功能。

from functools import singledispatch

# This class defines the function with
# a base case if the input type doesn't match
@singledispatch
def my_data_to_string(datum) -> str:
    raise TypeError(f"unsupported format: {type(datum)}")

# Registering for type str using type hint
@my_data_to_string.register
def _(datum: str):
    return datum

# Registering for multiple 
# types using decorator
@my_data_to_string.register(float)
@my_data_to_string.register(int)
def _(datum):
    return "<%s>" % datum


print(my_data_to_string("a"))    # a
print(my_data_to_string(1))      # <1>
print(my_data_to_string(1.5))    # <1.5>
print(my_data_to_string([1, 2])) # TypeError

它是可扩展的、可读的,并且不会在 linter/formatters 中产生错误。 Docs link.

【讨论】:

  • 虽然肯定是解决潜在问题的有效方法,但这似乎并不能真正回答问题。
  • 确实很有趣。但是,我看到了两个问题:1)我的代码在Union 中有更多类型和更多的函数来处理它(因此更多的模式匹配),这种语法意味着很多装饰器,以及许多额外的函数声明; 2)更重要的是,它要求函数的编写者了解Union的所有内部细节,以处理装饰器中的每个“结束”类型,这有点违背使用“高级”数据的想法类型。
【解决方案2】:

从 Python 3.10 开始,unions are valid for isinstance checks

def my_data_to_string(datum: MyDataT) -> str:
    if isinstance(datum, MyNumberT):
        return _my_number_to_string(datum)
    elif isinstance(datum, str):
        return datum
    # assert_never omitted for simplicity

只要排除一个工会的组成部分就足够了,无需要求即可撤销检查:

def my_data_to_string(datum: MyDataT) -> str:
    if isinstance(datum, str):  # handle explicit type first
        return datum
    else:  # catch-all for remaining types
        return _my_number_to_string(datum)
    # rely on type checker for safety!

请注意,这使用了else 而不是elif 子句——依靠类型检查器来拒绝输入错误的参数。


对于更复杂的类型,你可以构建一个类型保护:

def guard_mnt(arg: MyDataT) -> Union[Literal[False], Tuple[MyNumberT]]:
    return (arg,) if isinstance(arg, get_args(MyNumberT)) else False  # type: ignore

这告诉类型检查器它将返回所需的包装类型或错误的东西。 type: ignore 是必需的,因为它使用相同的类型检查实现;该函数用作围绕不受支持的运行时检查添加有效的静态类型检查。

可以通过赋值表达式和解包来使用:

def my_data_to_string(datum: MyDataT) -> str:
    if nums := guard_mnt(datum):  # only enter branch if guard is not False
        return _my_number_to_string(*datum)
    elif isinstance(datum, str):
        return datum
    # assert_never omitted for simplicity

【讨论】:

  • 类型保护看起来是个有趣的想法。虽然,有两件事我不明白:1)为什么# type: ignore 需要存在? 2) 为什么arg 被放在一个元组中......它可以直接返回(作为MyNumberT)吗?此外,assert_never 机制似乎被它破坏了("assert_never" has incompatible type "Union[float,int]"; expected "NoReturn" - 对应于MyNumberT...)。
  • #type: ignore 是必需的,因为守卫仍然使用与问题中相同的机制 (isinstance + get_args),类型检查器实际上并不理解,因此假定 (arg,)Tuple[MydataT]。如果类型匹配,则需要元组来确保结果为真; 00.0 都是假的,但 (0,)(0.0,) 都是真的。
猜你喜欢
  • 2011-08-11
  • 2022-01-11
  • 2020-03-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-12-16
  • 1970-01-01
相关资源
最近更新 更多