简体   繁体   English

Python 中嵌套“联合”类型的模式匹配

[英]Pattern matching over nested `Union` types in Python

Building a Python library, I am using type hints to guarantee consistency over certain data representation.构建一个 Python 库,我使用类型提示来保证某些数据表示的一致性。 In particular, I am making use of Union (sum types) in a nested fashion to represent the different "flavor" a datum can take.特别是,我以嵌套方式使用Union (求和类型)来表示数据可以采用的不同“风格”。

What I end up with so far is similar to the following example:到目前为止,我最终得到的类似于以下示例:

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

Which type-checks fine using mypy .使用mypy可以很好地进行类型检查。

Now, my real code is a bit more complex, and I need to perform some common operations on variables that are of type MyNumberT .现在,我的真实代码有点复杂,我需要对MyNumberT类型的变量执行一些常见操作。 In the example, this is simply highlighted by adapting the import and replacing my_data_to_string as in the following:在示例中,这只是通过调整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

[...]

On which the type-checking of mypy fails: Argument 1 to "_my_number_to_string" has incompatible type "Union[str, Union[float, int]]"; expected "Union[float, int]" mypy的类型检查失败: Argument 1 to "_my_number_to_string" has incompatible type "Union[str, Union[float, int]]"; expected "Union[float, int]" Argument 1 to "_my_number_to_string" has incompatible type "Union[str, Union[float, int]]"; expected "Union[float, int]" . Argument 1 to "_my_number_to_string" has incompatible type "Union[str, Union[float, int]]"; expected "Union[float, int]"

I expected mypy to "realise" that in the first branch, datum could only be of type float or int , but the error message indicates it's not the case...我希望mypy能够“意识到”在第一个分支中, datum只能是floatint类型,但错误消息表明情况并非如此......

How could I achieve some pattern matching over "parts" of such nested types?我怎样才能在这种嵌套类型的“部分”上实现一些模式匹配?

Your use-case is a great example to use a utility provided by functools called singledispatch .您的用例是使用functools提供的名为singledispatch的实用程序的一个很好的例子。 It allows you to define multiple functionality to a single function based on type of input.它允许您根据输入类型为单个 function 定义多种功能。

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

It is extensible, readable and doesn't generate error in linters/formatters.它是可扩展的、可读的,并且不会在 linter/formatters 中产生错误。 Docs link . 文档链接

Starting with Python 3.10, unions are valid for isinstance checks :从 Python 3.10 开始, 联合对 isinstance 检查有效

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

As long as it is sufficient to exclude one constituent of the union, reversing the check works without requirements:只要排除工会的一个组成部分就足够了,无需要求即可撤销检查:

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!

Notice that this uses an else instead of an elif clause – rely on the type checker to reject incorrectly typed arguments.请注意,这使用else而不是elif子句——依靠类型检查器拒绝错误类型的 arguments。


For more complex types, you can build a type guard:对于更复杂的类型,您可以构建类型保护:

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

This tells a type checker that it will either return the desired type wrapped or something false.这告诉类型检查器它将返回所需的包装类型或错误的东西。 The type: ignore is required since it uses the same type check implementation; type: ignore是必需的,因为它使用相同的类型检查实现; the function serves as add a valid static type check around the unsupported runtime check. function 用作围绕不受支持的运行时检查添加有效的 static 类型检查。

It can be used via assignment expressions and unpacking:它可以通过赋值表达式和解包来使用:

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM