而不是检查类型是否相等,应使用
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 inspectimport typingfrom contextlib import suppressfrom functools import wrapsdef 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.dataclassclass Point: x: float y: float@enforce_typesdef foo(bar: typing.Union[int, str]): pass
Appart通过验证上一节中建议的某些类型提示,此方法仍存在一些缺点:
- 使用字符串(类型提示
class Foo: def __init__(self: 'Foo'): pass
)不被考虑在内inspect.getfullargspec
:您可能希望使用typing.get_type_hints
和inspect.signature
替代; 不验证不是适当类型的默认值:
@enforce_type
def foo(bar: int = None):
passfoo()
没有筹集任何款项
TypeError。如果您想对此加以考虑
inspect.Signature.bind,
inspect.BoundArguments.apply_defaults则可能需要结合使用(并因此迫使您定义
deffoo(bar: typing.Optional[int] = None));
- 可变数量的参数无法通过验证,因为您必须定义类似的内容
def foo(*args: typing.Sequence, **kwargs: typing.Mapping)
,如开头所述,我们只能验证容器,而不能验证包含的对象。
更新资料
在这个答案开始流行之后,一个受它启发很大的图书馆发布了,消除上述缺点的需求已成为现实。因此,我对该
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``__origin__``typing._SpecialForm
拟议的改进:
import inspectimport typingfrom functools import wrapsdef _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 yield typing.Any 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_typedef _check_types(parameters, hints): for name, value in parameters.items(): type_hint = hints.get(name, typing.Any) actual_types = tuple( origin for origin in _find_type_origin(type_hint) if origin is not typing.Any ) 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)


