验证python数据类中的详细类型

发布于 2021-01-29 19:36:31

Python
3.7是在不久前发布的,我想测试一些新奇的dataclass+键入功能。使用本机类型和typing模块中的本机类型,使提示正确工作非常容易。

>>> import dataclasses
>>> import typing as ty
>>> 
... @dataclasses.dataclass
... class Structure:
...     a_str: str
...     a_str_list: ty.List[str]
...
>>> my_struct = Structure(a_str='test', a_str_list=['t', 'e', 's', 't'])
>>> my_struct.a_str_list[0].  # IDE suggests all the string methods :)

但是我想尝试的另一件事是在运行时强制将类型提示作为条件,即dataclass类型不正确的a不可能存在。它可以很好地实现__post_init__

>>> @dataclasses.dataclass
... class Structure:
...     a_str: str
...     a_str_list: ty.List[str]
...     
...     def validate(self):
...         ret = True
...         for field_name, field_def in self.__dataclass_fields__.items():
...             actual_type = type(getattr(self, field_name))
...             if actual_type != field_def.type:
...                 print(f"\t{field_name}: '{actual_type}' instead of '{field_def.type}'")
...                 ret = False
...         return ret
...     
...     def __post_init__(self):
...         if not self.validate():
...             raise ValueError('Wrong types')

这种validate功能适用于本机类型和自定义类,但不适用于typing模块指定的功能:

>>> my_struct = Structure(a_str='test', a_str_list=['t', 'e', 's', 't'])
Traceback (most recent call last):
  a_str_list: '<class 'list'>' instead of 'typing.List[str]'
  ValueError: Wrong types

有没有更好的方法来验证带有typing-typed列表的无类型列表?优选地,一个不包括检查类型的所有元素的任何listdicttuple,或set这是一个dataclass“属性。

关注者
0
被浏览
70
1 个回答
  • 面试哥
    面试哥 2021-01-29
    为面试而生,有面试问题,就找面试哥。

    而不是检查类型是否相等,应使用isinstance。但是您不能使用参数化的泛型类型(typing.List[int]),而必须使用“泛型”版本(typing.List)。因此,您将能够检查容器类型,而不是所包含的类型。参数化的泛型类型定义了__origin__可用于该属性的属性。

    与Python 3.6相反,在Python 3.7中,大多数类型提示都具有有用的__origin__属性。比较:

    # Python 3.6
    >>> import typing
    >>> typing.List.__origin__
    >>> typing.List[int].__origin__
    typing.List
    

    # Python 3.7
    >>> import typing
    >>> typing.List.__origin__
    <class 'list'>
    >>> typing.List[int].__origin__
    <class 'list'>
    

    Python
    3.8通过typing.get_origin()自省功能引入了更好的支持:

    # Python 3.8
    >>> import typing
    >>> typing.get_origin(typing.List)
    <class 'list'>
    >>> typing.get_origin(typing.List[int])
    <class 'list'>
    

    值得注意的例外是typing.Anytyping.Uniontyping.ClassVar…嗯,任何一个typing._SpecialForm没有定义__origin__。幸好:

    >>> isinstance(typing.Union, typing._SpecialForm)
    True
    >>> isinstance(typing.Union[int, str], typing._SpecialForm)
    False
    >>> typing.get_origin(typing.Union[int, str])
    typing.Union
    

    但是参数化类型定义了一个__args__属性,该属性将其参数存储为元组。Python
    3.8引入了typing.get_args()检索它们的功能:

    # Python 3.7
    >>> typing.Union[int, str].__args__
    (<class 'int'>, <class 'str'>)
    
    # Python 3.8
    >>> typing.get_args(typing.Union[int, str])
    (<class 'int'>, <class 'str'>)
    

    因此,我们可以改进类型检查:

    for field_name, field_def in self.__dataclass_fields__.items():
        if isinstance(field_def.type, typing._SpecialForm):
            # No check for typing.Any, typing.Union, typing.ClassVar (without parameters)
            continue
        try:
            actual_type = field_def.type.__origin__
        except AttributeError:
            # In case of non-typing types (such as <class 'int'>, for instance)
            actual_type = field_def.type
        # In Python 3.8 one would replace the try/except with
        # actual_type = typing.get_origin(field_def.type) or field_def.type
        if isinstance(actual_type, typing._SpecialForm):
            # case of typing.Union[…] or typing.ClassVar[…]
            actual_type = field_def.type.__args__
    
        actual_value = getattr(self, field_name)
        if not isinstance(actual_value, actual_type):
            print(f"\t{field_name}: '{type(actual_value)}' instead of '{field_def.type}'")
            ret = False
    

    这不是完美的,因为它不会考虑typing.ClassVar[typing.Union[int, str]]typing.Optional[typing.List[int]]为实例,但是它应该上手的东西。


    接下来是应用此检查的方法。

    除了使用之外__post_init__,我还可以使用装饰器路线:这可以用于具有类型提示的任何东西,不仅限于dataclasses

    import inspect
    import typing
    from contextlib import suppress
    from functools import wraps
    
    
    def enforce_types(callable):
        spec = inspect.getfullargspec(callable)
    
        def check_types(*args, **kwargs):
            parameters = dict(zip(spec.args, args))
            parameters.update(kwargs)
            for name, value in parameters.items():
                with suppress(KeyError):  # Assume un-annotated parameters can be any type
                    type_hint = spec.annotations[name]
                    if isinstance(type_hint, typing._SpecialForm):
                        # No check for typing.Any, typing.Union, typing.ClassVar (without parameters)
                        continue
                    try:
                        actual_type = type_hint.__origin__
                    except AttributeError:
                        # In case of non-typing types (such as <class 'int'>, for instance)
                        actual_type = type_hint
                    # In Python 3.8 one would replace the try/except with
                    # actual_type = typing.get_origin(type_hint) or type_hint
                    if isinstance(actual_type, typing._SpecialForm):
                        # case of typing.Union[…] or typing.ClassVar[…]
                        actual_type = type_hint.__args__
    
                    if not isinstance(value, actual_type):
                        raise TypeError('Unexpected type for \'{}\' (expected {} but found {})'.format(name, type_hint, type(value)))
    
        def decorate(func):
            @wraps(func)
            def wrapper(*args, **kwargs):
                check_types(*args, **kwargs)
                return func(*args, **kwargs)
            return wrapper
    
        if inspect.isclass(callable):
            callable.__init__ = decorate(callable.__init__)
            return callable
    
        return decorate(callable)
    

    用法是:

    @enforce_types
    @dataclasses.dataclass
    class Point:
        x: float
        y: float
    
    @enforce_types
    def foo(bar: typing.Union[int, str]):
        pass
    

    Appart通过验证上一节中建议的某些类型提示,此方法仍存在一些缺点:

    • 使用字符串(类型提示class Foo: def __init__(self: 'Foo'): pass)不被考虑在内inspect.getfullargspec:您可能希望使用typing.get_type_hintsinspect.signature替代;
    • 不验证不是适当类型的默认值:

      @enforce_type
      

      def foo(bar: int = None):
      pass

      foo()

    没有筹集任何款项TypeError。如果您想对此加以考虑inspect.Signature.bindinspect.BoundArguments.apply_defaults则可能需要结合使用(并因此迫使您定义def foo(bar: typing.Optional[int] = None));

    • 可变数量的参数无法通过验证,因为您必须定义类似的内容def foo(*args: typing.Sequence, **kwargs: typing.Mapping),如开头所述,我们只能验证容器,而不能验证包含的对象。

    更新资料

    在这个答案开始流行之后,一个受它启发很大的图书馆发布了,消除上述缺点的需求已成为现实。因此,我对该typing模块进行了更多介绍,并将在此处提出一些发现和一种新方法。

    对于初学者来说,typing在寻找参数何时可选方面做得很好:

    >>> def foo(a: int, b: str, c: typing.List[str] = None):
    ...   pass
    ... 
    >>> typing.get_type_hints(foo)
    {'a': <class 'int'>, 'b': <class 'str'>, 'c': typing.Union[typing.List[str], NoneType]}
    

    这非常好,绝对是对的改进inspect.getfullargspec,因此最好使用它,因为它还可以正确地将字符串作为类型提示来处理。但是typing.get_type_hints会为其他类型的默认值提供援助:

    >>> def foo(a: int, b: str, c: typing.List[str] = 3):
    ...   pass
    ... 
    >>> typing.get_type_hints(foo)
    {'a': <class 'int'>, 'b': <class 'str'>, 'c': typing.List[str]}
    

    因此,即使您觉得这种情况非常麻烦,您仍可能需要进行更严格的检查。

    接下来是用作或typing参数的提示的情况。由于这些中的始终是一个元组,因此可以递归地找到该元组中包含的提示的。结合以上检查,我们将需要过滤所有剩余的内容。typing._SpecialForm``typing.Optional[typing.List[str]]``typing.Final[typing.Union[typing.Sequence, typing.Mapping]]``__args__``typing._SpecialForm``__origin__``typing._SpecialForm

    拟议的改进:

    import inspect
    import typing
    from functools import wraps
    
    
    def _find_type_origin(type_hint):
        if isinstance(type_hint, typing._SpecialForm):
            # case of typing.Any, typing.ClassVar, typing.Final, typing.Literal,
            # typing.NoReturn, typing.Optional, or typing.Union without parameters
            yield typing.Any
            return
    
        actual_type = typing.get_origin(type_hint) or type_hint  # requires Python 3.8
        if isinstance(actual_type, typing._SpecialForm):
            # case of typing.Union[…] or typing.ClassVar[…] or …
            for origins in map(_find_type_origin, typing.get_args(type_hint)):
                yield from origins
        else:
            yield actual_type
    
    
    def _check_types(parameters, hints):
        for name, value in parameters.items():
            type_hint = hints.get(name, typing.Any)
            actual_types = tuple(
                    origin
                    for origin in _find_type_origin(type_hint)
                    if origin is not typing.Any
            )
            if actual_types and not isinstance(value, actual_types):
                raise TypeError(
                        f"Expected type '{type_hint}' for argument '{name}'"
                        f" but received type '{type(value)}' instead"
                )
    
    
    def enforce_types(callable):
        def decorate(func):
            hints = typing.get_type_hints(func)
            signature = inspect.signature(func)
    
            @wraps(func)
            def wrapper(*args, **kwargs):
                parameters = dict(zip(signature.parameters, args))
                parameters.update(kwargs)
                _check_types(parameters, hints)
    
                return func(*args, **kwargs)
            return wrapper
    
        if inspect.isclass(callable):
            callable.__init__ = decorate(callable.__init__)
            return callable
    
        return decorate(callable)
    
    
    def enforce_strict_types(callable):
        def decorate(func):
            hints = typing.get_type_hints(func)
            signature = inspect.signature(func)
    
            @wraps(func)
            def wrapper(*args, **kwargs):
                bound = signature.bind(*args, **kwargs)
                bound.apply_defaults()
                parameters = dict(zip(signature.parameters, bound.args))
                parameters.update(bound.kwargs)
                _check_types(parameters, hints)
    
                return func(*args, **kwargs)
            return wrapper
    
        if inspect.isclass(callable):
            callable.__init__ = decorate(callable.__init__)
            return callable
    
        return decorate(callable)
    


知识点
面圈网VIP题库

面圈网VIP题库全新上线,海量真题题库资源。 90大类考试,超10万份考试真题开放下载啦

去下载看看