Python类型化:文本类型化参数的验证修饰符

2024-04-26 10:01:04 发布

您现在位置:Python中文网/ 问答频道 /正文

我经常遇到函数只接受有限的值集的情况。我知道如何使用typing.Literal在类型注释中反映此行为,如下所示:

import typing


def func(a: typing.Literal['foo', 'bar']):
    pass

我希望有一个decorator @validate_literals,用于验证的参数是否与其类型一致:

@validate_literals
def picky_typed_function(
    binary: typing.Literal[0, 1],
    char: typing.Literal['a', 'b']
) -> None:
    pass

因此,根据参数类型定义的限制验证输入,并在发生冲突时引发ValueError

picky_typed_function(0, 'a')  # should pass
picky_typed_function(2, 'a')  # should raise "ValueError: binary must be one of (0, 1)')"
picky_typed_function(0, 'c')  # should raise "ValueError: char must be one of ('a', 'b')"
picky_typed_function(0, char='c')  # should raise "ValueError: char must be one of ('a', 'b')"
picky_typed_function(binary=2, char='c')  # should raise "ValueError: binary must be one of (0, 1)"

typing类型检查被设计为静态的,不会在运行时发生。如何利用类型定义进行运行时验证


Tags: oftyping类型functionbeoneraisepicky
2条回答

我们可以使用^{}检查修饰(验证)函数的签名,通过^{}(或者,对于python版本<;3.8,使用^{})获取参数注释的“原点”,检查函数的哪些参数是作为文本别名键入的,并使用[typing.get_args()](https://stackoverflow.com/a/64522240/3566606)检索有效值(并在嵌套的文字定义上递归迭代)从文字别名

为了做到这一点,剩下要做的就是找出哪些参数已作为位置参数传递,并将相应的值映射到参数的名称,以便可以将该值与参数的有效值进行比较

最后,我们使用带有^{}的标准配方构建装饰器

import inspect
import typing
import functools


def args_to_kwargs(func: typing.Callable, *args: list, **kwargs: dict) -> dict:
    args_dict = {
        list(inspect.signature(func).parameters.keys())[i]: arg
        for i, arg in enumerate(args)
    }

    return {**args_dict, **kwargs}


def valid_args_from_literal(annotation: _GenericAlias) -> Set[Any]:
    args = get_args(annotation)
    valid_values = []

    for arg in args:
        if typing.get_origin(parameter.annotation) is Literal:
            valid_values += valid_args_from_literal(arg)
        else:
            valid_values += [arg]

    return set(valid_values)


def validate_literals(func: typing.Callable) -> typing.Callable:
    @functools.wraps(func)
    def validated(*args, **kwargs):
        kwargs = args_to_kwargs(func, *args, **kwargs)
        for name, parameter in inspect.signature(func).parameters.items():
            # use parameter.annotation.__origin__ for Python versions < 3.8
            if typing.get_origin(parameter.annotation) is typing.Literal:
                valid_values = valid_args_from_literal(parameter.annotation)
                if kwargs[name] not in valid_values:
                    raise ValueError(
                        f"Argument '{name}' must be one of {valid_values}"
                    )

        return func(**kwargs)

    return validated

这给出了问题中指定的结果

我还发布了python包的alpha版本runtime-typing来执行运行时类型检查:https://pypi.org/project/runtime-typing/(文档:https://runtime-typing.readthedocs.io),它处理的情况比typing.Literal多,例如typing.TypeVartyping.Union

from typing import Literal
from valdec.dec import validate

@validate
def func(a: Literal["foo", "bar"]) -> str:
    return a

assert func("bar") == "bar"


@validate("return", exclude=True)
def func(binary: Literal[0, 1], char: Literal["a", "b"]):
    return binary, char

assert func(0, "a") == (0, "a")


func(2, "x")
# valdec.utils.ValidationArgumentsError: Validation error <class 
# 'valdec.val_pydantic.ValidationError'>: 2 validation errors for argument 
# with the name of:
# binary
#   unexpected value; permitted: 0, 1 (type=value_error.const; given=2; 
#   permitted=(0, 1))
# char
#   unexpected value; permitted: 'a', 'b' (type=value_error.const; given=x; 
#   permitted=('a', 'b')).

瓦尔德克:https://github.com/EvgeniyBurdin/valdec

相关问题 更多 >