验证 python 个数据类中的详细类型
Validating detailed types in python dataclasses
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
函数适用于本机类型和自定义类型 类,但不适用于 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
' 属性。
几年后重新审视这个问题,我现在开始使用 pydantic
来验证 类 我通常只为其定义一个数据类。不过,我会在当前接受的答案上留下我的印记,因为它正确回答了原始问题并且具有突出的教育价值。
您应该使用 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.Any
、typing.Union
和 typing.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
除了验证上一节中建议的某些类型提示外,此方法仍有一些缺点:
inspect.getfullargspec
未考虑使用字符串 (class Foo: def __init__(self: 'Foo'): pass
) 的类型提示:您可能想改用 typing.get_type_hints
and inspect.signature
;
未验证类型不正确的默认值:
@enforce_type
def foo(bar: int = None):
pass
foo()
没有提出任何 TypeError
。如果你想考虑到这一点,你可能想使用 inspect.Signature.bind
in conjuction with 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]]
。由于这些 typing._SpecialForm
的 __args__
始终是一个元组,因此可以递归地找到该元组中包含的提示的 __origin__
。结合上述检查,我们将需要过滤掉任何 typing._SpecialForm
left.
提议的改进:
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 帮助我改进了这个答案。
刚发现这个问题。
pydantic 可以开箱即用地对数据类进行完整类型验证。 (承认:我建立了 pydantic)
只需使用 pydantic 版本的装饰器,生成的数据类完全是普通的。
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)
对于键入别名,您必须单独检查注释。
我确实喜欢这样:
https://github.com/EvgeniyBurdin/validated_dc
我为此创建了一个小型 Python 库:https://github.com/tamuhey/dataclass_utils
这个库可以应用于包含另一个数据类(嵌套数据类)和嵌套容器类型(如Tuple[List[Dict...
)的数据类
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
函数适用于本机类型和自定义类型 类,但不适用于 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
' 属性。
几年后重新审视这个问题,我现在开始使用 pydantic
来验证 类 我通常只为其定义一个数据类。不过,我会在当前接受的答案上留下我的印记,因为它正确回答了原始问题并且具有突出的教育价值。
您应该使用 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.Any
、typing.Union
和 typing.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
除了验证上一节中建议的某些类型提示外,此方法仍有一些缺点:
inspect.getfullargspec
未考虑使用字符串 (class Foo: def __init__(self: 'Foo'): pass
) 的类型提示:您可能想改用typing.get_type_hints
andinspect.signature
;未验证类型不正确的默认值:
@enforce_type def foo(bar: int = None): pass foo()
没有提出任何
TypeError
。如果你想考虑到这一点,你可能想使用inspect.Signature.bind
in conjuction withinspect.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]]
。由于这些 typing._SpecialForm
的 __args__
始终是一个元组,因此可以递归地找到该元组中包含的提示的 __origin__
。结合上述检查,我们将需要过滤掉任何 typing._SpecialForm
left.
提议的改进:
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 帮助我改进了这个答案。
刚发现这个问题。
pydantic 可以开箱即用地对数据类进行完整类型验证。 (承认:我建立了 pydantic)
只需使用 pydantic 版本的装饰器,生成的数据类完全是普通的。
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)
对于键入别名,您必须单独检查注释。 我确实喜欢这样: https://github.com/EvgeniyBurdin/validated_dc
我为此创建了一个小型 Python 库:https://github.com/tamuhey/dataclass_utils
这个库可以应用于包含另一个数据类(嵌套数据类)和嵌套容器类型(如Tuple[List[Dict...
)的数据类