[英]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
只能是float
或int
类型,但错误消息表明情况并非如此......
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.