动态创建文本别名的输入方式:从有效值列表中

68
我有一个函数,它验证其参数只接受给定有效选项列表中的值。在类型方面,我使用了“字面量”类型别名来反映这种行为,如下所示:
```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中。如何根据VALID_ARGUMENTS变量动态创建Argument Literal类型?

以下方法不起作用

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

那么,如何实现呢?或者根本无法实现吗?


4
你差不多做到了!Literal 接受类型或字面量的元组。ValidArgs = Literal[tuple(VALID_ARGUMENTS)] 可以工作。但正如之前提到的,它会破坏静态类型检查器。 - kuza
当然,这肯定会击败静态类型检查器。这里的目标就是自相矛盾。动态创建类型意味着直到代码运行之前都无法发生。而静态类型检查器的整个目的就是在代码运行之前执行检查。 - Karl Knechtel
4个回答

67

反过来,从Argument构建VALID_ARGUMENTS

Argument = typing.Literal['foo', 'bar']
VALID_ARGUMENTS: typing.Tuple[Argument, ...] = typing.get_args(Argument)

在运行时,可以从 VALID_ARGUMENTS 构建 Argument,但这样做与静态分析不兼容,而静态分析是类型注释的主要用途。从 Argument 构建 VALID_ARGUMENTS 是最好的选择。

这里我使用了元组作为 VALID_ARGUMENTS,但如果出于某些原因你真的更喜欢列表,你也可以得到一个:

VALID_ARGUMENTS: typing.List[Argument] = list(typing.get_args(Argument))

2
typing.Tuple[Argument, ...]中省略号的目的是什么? - N4v
10
这是一个同构的、长度任意的元组的语法。 - user2357112
@Barzi2001:在 mypy playground 上运行正常。(https://mypy-play.net/?mypy=latest&python=3.10&gist=d0344d894d0458d1c2bbbdc0b7d7c555) 也许您其中一个注释写错了。 - user2357112
是的,我使用了圆括号而不是方括号。谢谢你的提示。但说实话,这种语法真的很丑陋。 - Barzi2001
这是一种不错的方法,但你会丢失类型信息。如果你手动输入 VALID_ARGUMENTS = "foo", "bar",你会得到 tuple[Literal["foo"], Literal["bar"]] 而不是 tuple[Argument, ...],所以我认为最好的方法仍然是手动重新编写一遍所有内容。 - Fayeure
显示剩余2条评论

28
如果有人仍在寻找这个问题的解决方案:
typing.Literal[tuple(VALID_ARGUMENTS)]

14
根据mypy文档:“Literal类型可以包含一个或多个文字布尔值,整数,字符串,字节和枚举值。然而,字面量类型不能包含任意表达式:像 Literal[my_string.trim()]Literal[x > 3]Literal[3j + 4] 这样的类型都是非法的。” 因此,这是有效的python语法,但不会被任何类型检查器理解,这完全违背了首次添加类型提示的初衷。https://mypy.readthedocs.io/en/stable/literal_types.html#limitations - Alex Waygood
6
绝对不会违背初衷。在第一时间,打字可以被看作是有结构的文档记录。这绝对解决了这个问题。 - Marc
1
@alex-waygood,有一些框架也使用类型提示。例如,为了在FastAPI Swagger UI/文档中半动态地包含允许值列表,目前这几乎是唯一的方法。虽然不太好看,但在某些情况下比硬编码值要好。 - miksus
2
我无法编辑我的评论,但是我同意,如果您使用类型提示进行运行时目的而不是静态类型,则仍然可以很有用。然而,在涉及Python类型的问题上,我认为可以合理地假设作者正在寻找一个能够满足静态类型检查器的解决方案,除非另有说明。到目前为止,类型提示最常见的用途是用于静态类型检查。 - Alex Waygood
@Marc,我明白你的意思,但我不同意。静态分析是类型检查的一个非常重要的部分。我认为“在第一时间”这个说法并不准确。包括看起来像提供静态分析的提示,但实际上并没有,会导致混淆。 - joel
这还有效吗?我收到了Literal的类型参数必须是None、字面值(int、bool、str或bytes)或枚举值的错误信息[Pylance]。 - undefined

1
扩展@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 vs 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

-5

这是针对这个问题的解决方法。但不知道是否是一个好的解决方案。

VALID_ARGUMENTS = ['foo', 'bar']

Argument = Literal['1']

Argument.__args__ = tuple(VALID_ARGUMENTS)

print(Argument)
# typing.Literal['foo', 'bar']

5
由于这对之前定义的静态类型进行了运行时修改,因此它根本无法正常工作。运行时类型将为“Literal [ 'foo', 'bar']”,但用于验证的静态类型仍然是“Literal [ '1']”。 - MisterMiyagi
@MisterMiyagi 我使用FastAPI + Pydantic的解决方案。它对于字段验证非常有效。我只能通过POST请求将foobar值传递给我的API,而不能传递1值。 - Nairum
@AlexeiMarinichenko 这很好,但这并不改变它在预期使用中根本无法工作的事实。问题已经展示了两种情况,在运行时也能正常工作,但比玩弄特定于实现的内部要更加健壮。 - MisterMiyagi

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接