gpt4 book ai didi

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

转载 作者:太空狗 更新时间:2023-10-29 17:03:07 27 4
gpt4 key购买 nike

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 不应该是可能的。存在不正确的类型。它可以很好地实现 __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函数适用于 native 类型和自定义类,但不适用于 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 的无类型列表? -输入一个?最好不包括检查任何 list 中所有元素的类型。 , dict , tuple , 或 set那是 dataclass ' 属性。

最佳答案

您应该使用 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() 自省(introspection)功能:
# Python 3.8
>>> import typing
>>> typing.get_origin(typing.List)
<class 'list'>
>>> typing.get_origin(typing.List[int])
<class 'list'>
值得注意的异常(exception)是 typing.Any , typing.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
除了验证上一节中建议的一些类型提示之外,这种方法仍然有一些缺点:
  • 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) 的内容而且,正如开头所说,我们只能验证容器,而不能验证包含的对象。

  • 更新
    在这个答案获得一些人气和一个 library 之后受到它发布的极大启发,消除上述缺点的需求正在成为现实。所以我用 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 s 总是一个元组,可以递归地找到 __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
    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(_find_type_origin(type_hint))
    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)

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

    关于python - 验证 python 数据类中的详细类型,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/50563546/

    27 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com