根据有效值列表动态创建文字别名
- 2025-02-27 09:07:00
- admin 原创
- 61
问题描述:
我有一个函数,它验证其参数是否仅接受给定有效选项列表中的值。从类型方面来说,我使用Literal
类型别名来反映此行为,如下所示:
from typing import Literal
VALID_ARGUMENTS = ['foo', 'bar']
Argument = Literal['foo', 'bar']
def func(argument: 'Argument') -> None:
if argument not in VALID_ARGUMENTS:
raise ValueError(
f'argument must be one of {VALID_ARGUMENTS}'
)
# ...
这违反了 DRY 原则,因为我必须重写我的 Literal 类型定义中的有效参数列表,即使它已经存储在变量中。给定变量,VALID_ARGUMENTS
如何动态创建Literal 类型?Argument
`VALID_ARGUMENTS`
以下操作无效:
from typing import Literal, Union, NewType
Argument = Literal[*VALID_ARGUMENTS] # SyntaxError: invalid syntax
Argument = Literal[VALID_ARGUMENTS] # Parameters to generic types must be types
Argument = Literal[Union[VALID_ARGUMENTS]] # TypeError: Union[arg, ...]: each arg must be a type. Got ['foo', 'bar'].
Argument = NewType(
'Argument',
Union[
Literal[valid_argument]
for valid_argument in VALID_ARGUMENTS
]
) # Expected type 'Type[_T]', got 'list' instead
这到底能不能做到?
解决方案 1:
反过来,VALID_ARGUMENTS
从以下开始构建Argument
:
Argument = typing.Literal['foo', 'bar']
VALID_ARGUMENTS: typing.Tuple[Argument, ...] = typing.get_args(Argument)
我在VALID_ARGUMENTS
这里使用了一个元组,但是如果由于某种原因你确实更喜欢列表,你可以得到一个:
VALID_ARGUMENTS: typing.List[Argument] = list(typing.get_args(Argument))
可以在运行时Argument
从 进行构建VALID_ARGUMENTS
,但这样做与静态分析不兼容,而静态分析是类型注释的主要用例。
这样做也被认为是语义无效的——规范禁止Literal
使用动态计算的参数进行参数化。运行时实现根本没有验证这一点所需的信息。VALID_ARGUMENTS
从中构建Argument
才是可行的方法。
解决方案 2:
如果有人仍在寻找解决这个问题的方法:
typing.Literal[tuple(VALID_ARGUMENTS)]
解决方案 3:
扩展@user2357112 的答案......可以为"foo"
和的各个字符串创建变量"bar"
。
from __future__ import annotations
from typing import get_args, Literal, TypeAlias
T_foo = Literal['foo']
T_bar = Literal['bar']
T_valid_arguments: TypeAlias = T_foo | T_bar
FOO: T_foo = get_args(T_foo)[0]
BAR: T_bar = get_args(T_bar)[0]
VALID_ARGUMENTS = (FOO, BAR)
def func(argument: T_valid_arguments) -> None:
if argument not in VALID_ARGUMENTS:
raise ValueError(f"argument must be one of {VALID_ARGUMENTS}")
#mypy checks
func(FOO) # OK
func('foo') # OK
func('baz') # error: Argument 1 to "func" has incompatible type "Literal['baz']"; expected "Literal['foo', 'bar']" [arg-type]
reveal_type(FOO) # note: Revealed type is "Literal['foo']"
reveal_type(BAR). # note: Revealed type is "Literal['bar']"
reveal_type(VALID_ARGUMENTS) # note: Revealed type is "tuple[Literal['foo'], Literal['bar']]"
但是,有人可能会认为,get_args
在这种情况下使用是过度的,以避免在代码中输入两次字符串"foo"
。(关于:DRY 与 WET)您可以轻松地执行以下操作并获得相同的结果。
from __future__ import annotations
from typing import Literal, TypeAlias
T_foo = Literal['foo']
T_bar = Literal['bar']
T_valid_arguments: TypeAlias = T_foo | T_bar
FOO: T_foo = 'foo'
BAR: T_bar = 'bar'
VALID_ARGUMENTS = (FOO, BAR)
使用字符串作为注释时需要注意Literal
。Mypy 会对此发出警告:
FOO = 'foo'
def func(argument: T_valid_arguments) -> None:
...
func(FOO) # error: Argument 1 to "func" has incompatible type "str"; expected "Literal['foo', 'bar']" [arg-type]
但下面的就没问题。
func('foo') # OK
解决方案 4:
看来 Python 官方已经意识到这个功能确实很有用,从 3.11 开始我们可以在以下位置使用可变参数Literal
:
from typing import Literal, Any
from inspect import signature
def foo(snap: int, crackle: str = 'hello', pop: float = 3.14) -> None:
pass
valid_values = list(signature(foo).parameters)
FooArgname = Literal[*valid_values]
assert FooArgname == Literal['snap', 'crackle', 'pop']
# example usage: making a type for valid kwargs for foo
ValidFooKwargs = dict[FooArgname, Any]
在上述示例中,“动态”定义有效值列表的用途显而易见:我们希望有一个FooArgname
与对象(此处foo
)对齐的类型。当我们必须“静态”定义此列表时,我们就不是 DRY 并且冒着与目标对象不一致的风险。
Literal[tuple(valid_values)]
上面 Chris Goddard 提到的解决方案执行时没有语法错误,但我的 linter 仍然以红色显示(运行 3.10 时)。
附录
为了完整起见,由于第一条评论引用了我的原始代码,因此它如下:
from typing import Literal
from inspect import signature, Parameter
valid_values = list(signature(Parameter).parameters)
ParamAttribute = Literal[*valid_values]
assert ParamAttribute == Literal['name', 'kind', 'default', 'annotation']
解决方案 5:
一种解决方案可能是使用StrEnum
而不是文字,如果需要,可以将其转换为列表:
from enum import StrEnum
class Argument(StrEnum):
foo = "foo"
bar = "bar"
VALID_ARGUMENTS = [el.value for el in Argument]
def func(argument: Argument) -> None:
if argument not in VALID_ARGUMENTS:
raise ValueError(
f'argument must be one of {VALID_ARGUMENTS}'
)
# ...
解决方案 6:
如果你使用的是 python3.9,那么这个也可以:
internal = "', '".join(key for key in VALID_ARGUMENTS)
Argument = eval(f"Literal['{internal}']")
del(internal)
解决方案 7:
这是解决此问题的办法。但不知道这是否是一个好的解决方案。
VALID_ARGUMENTS = ['foo', 'bar']
Argument = Literal['1']
Argument.__args__ = tuple(VALID_ARGUMENTS)
print(Argument)
# typing.Literal['foo', 'bar']
扫码咨询,免费领取项目管理大礼包!