ICode9

精准搜索请尝试: 精确搜索
首页 > 编程语言> 文章详细

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

2019-09-26 00:55:45  阅读:330  来源: 互联网

标签:python typing python-dataclasses


Python 3.7 is around the corner,我想测试一些奇特的新数据类型输入功能.使用本机类型和来自输入模块的类型,可以很容易地获得正确工作的提示:

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

这种验证函数适用于本机类型和自定义类,但不适用于键入模块指定的类型:

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

是否有更好的方法来验证带有打字类型的无类型列表?优选地,不包括检查作为数据类属性的任何列表,字典,元组或集合中的所有元素的类型.

解决方法:

您应该使用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'>

值得注意的例外是打字.任何,打字.联盟和打字.ClassVar ……好吧,任何打字._SpecialForm都没有定义__origin__.幸好:

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

这并不完美,因为它不会考虑输入.ClassVar [typing.Union [int,str]]或者输入.例如,可以选择[typing.List [int]],但它应该开始.

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

而不是使用__post_init__,我会去装饰器路线:这可以用于任何带有类型提示的东西,而不仅仅是数据类:

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

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

>使用字符串输入类型提示(类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)的内容,并且如开头所述,我们只能验证容器和不包含对象.

感谢@Aran-Fey帮我改进了这个答案.

标签:python,typing,python-dataclasses
来源: https://codeday.me/bug/20190926/1817976.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有