您好, 欢迎来到 !    登录 | 注册 | | 设为首页 | 收藏本站

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

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

而不是检查类型是否相等,应使用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通过验证上一节中建议的某些类型提示,此方法仍存在一些缺点:

不验证不是适当类型的认值:

@enforce_type

def foo(bar: int = None): pass

foo()

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

python 2022/1/1 18:32:49 有216人围观

撰写回答


你尚未登录,登录后可以

和开发者交流问题的细节

关注并接收问题和回答的更新提醒

参与内容的编辑和改进,让解决方法与时俱进

请先登录

推荐问题


联系我
置顶