代码之家  ›  专栏  ›  技术社区  ›  Arne

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

  •  15
  • Arne  · 技术社区  · 6 年前

    Python 3.7 is around the corner 我想测试一下 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 :)
    

    但我想尝试的另一件事是在运行时将类型提示作为条件强制执行,即 数据类 存在不正确的类型。它可以很好地实现 __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 函数适用于本机类型和自定义类,但不适用于由 打字 模块:

    >>> 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
    

    有没有更好的方法用 打字 -打了一个?最好是不包括检查任何 list , dict , tuple ,或者 set 那是一个 数据类 '属性。

    2 回复  |  直到 6 年前
        1
  •  25
  •   howaryoo Mathias Ettinger    6 年前

    您应该使用 isinstance 。但不能使用参数化的泛型类型( typing.List[int] )为此,必须使用“通用”版本( typing.List )因此,您可以检查容器类型,但不能检查包含的类型。参数化的泛型类型定义 __origin__ 可以用于此目的的属性。

    与python 3.6相反,在python3.7中,大多数类型提示都有一个有用的 _来源__ 属性。比较:

    # 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'>
    

    值得注意的例外是 typing.Any , typing.Union typing.ClassVar 好吧,只要是 typing._SpecialForm 不定义 _来源__ . 幸运的是:

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

    但是参数化类型定义了 __args__ 将参数存储为元组的属性:

    >>> typing.Union[int, str].__args__
    (<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:
            actual_type = 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__ ,我将转到decorator路径:这不仅可以用于具有类型提示的任何内容 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:
                        actual_type = 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
    

    通过验证前一节中建议的一些类型提示,这种方法仍然有一些缺点:

    • 使用字符串键入提示( class Foo: def __init__(self: 'Foo'): pass )未考虑到 inspect.getfullargspec typing.get_type_hints inspect.signature 相反;
    • 未验证不是适当类型的默认值:

      @enforce_type
      def foo(bar: int = None):
          pass
      
      foo()
      

      不升高任何 TypeError 。你可能想用 inspect.Signature.bind 与…结合 inspect.BoundArguments.apply_defaults 如果你想解释这一点(从而迫使你 def foo(bar: typing.Optional[int] = None) ;

    • 变量个数的参数无法验证,因为您必须定义 def foo(*args: typing.Sequence, **kwargs: typing.Mapping) 而且,正如前面所说,我们只能验证容器而不能包含对象。

    多亏了 @Aran-Fey 这帮助我改进了这个答案。

        2
  •  1
  •   Samuel Colvin    5 年前

    刚刚发现这个问题。

    pydantic 无法对开箱即用的数据类进行完整类型验证。(门票:我造了但丁)

    只要使用Pydantic版本的decorator,得到的数据类就完全是普通的了。

    from datetime import datetime
    from pydantic.dataclasses import dataclass
    
    @dataclass
    class User:
        id: int
        name: str = 'John Doe'
        signup_ts: datetime = None
    
    print(User(id=42, signup_ts='2032-06-21T12:00'))
    """
    User(id=42, name='John Doe', signup_ts=datetime.datetime(2032, 6, 21, 12, 0))
    """
    
    User(id='not int', signup_ts='2032-06-21T12:00')
    

    最后一行将给出:

        ...
    pydantic.error_wrappers.ValidationError: 1 validation error
    id
      value is not a valid integer (type=type_error.integer)