从有效值列表动态创建文字别名

问题描述 投票:0回答:5

我有一个函数,可以验证其参数以仅接受给定的有效选项列表中的值。在打字方面,我使用

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
中。给定
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

到底能不能做到?

python python-typing
5个回答
84
投票

反过来,从

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
,但这样做与静态分析不兼容,而静态分析是类型注释的主要用例。

这样做在语义上也被认为是无效的 - spec 禁止使用动态计算的参数对

Literal
进行参数化。运行时实现根本没有验证这一点所需的信息。从
VALID_ARGUMENTS
构建
Argument
才是正确的道路。


35
投票

如果有人仍在寻找解决方法:

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"
两次是多余的。 (回复:干与湿)您可以轻松地执行以下操作,获得相同的结果。

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

1
投票

看来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
)。当我们必须“静态”定义这个列表时,我们并不干燥,并且存在与目标对象不一致的风险。

上面 Chris Goddard 提到的

Literal[tuple(valid_values)]
解决方案执行时没有语法错误,但我的 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
投票

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

VALID_ARGUMENTS = ['foo', 'bar']

Argument = Literal['1']

Argument.__args__ = tuple(VALID_ARGUMENTS)

print(Argument)
# typing.Literal['foo', 'bar']
© www.soinside.com 2019 - 2024. All rights reserved.