不要检查类型相等,应该使用 isinstance
。但是你不能使用参数化的泛型类型 (typing.List[int]
) 进行操作,必须使用“通用”版本 (typing.List
)。这样就可以检查容器类型而不是包含的类型。参数化泛型类型定义了一个 __origin__
属性,你可以使用它来实现。
与Python 3.6不同,在Python 3.7中,大多数类型提示都有一个有用的 __origin__
属性。比较一下:
>>> 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()
函数来检索它们:
>>> typing.Union[int, str].__args__
(<class 'int'>, <class 'str'>)
>>> 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):
continue
try:
actual_type = field_def.type.__origin__
except AttributeError:
actual_type = field_def.type
if isinstance(actual_type, typing._SpecialForm):
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):
type_hint = spec.annotations[name]
if isinstance(type_hint, typing._SpecialForm):
continue
try:
actual_type = type_hint.__origin__
except AttributeError:
actual_type = type_hint
if isinstance(actual_type, typing._SpecialForm):
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
除了在前面一节中建议的验证某些类型提示之外,这种方法仍然有一些缺点:
type hints using strings (class Foo: def __init__(self: 'Foo'): pass
) are not taken into account by inspect.getfullargspec
: you may want to use typing.get_type_hints
and inspect.signature
instead;
a default value which is not the appropriate type is not validated:
@enforce_type
def foo(bar: int = None):
pass
foo()
does not raise any TypeError
. You may want to use inspect.Signature.bind
in conjuction with inspect.BoundArguments.apply_defaults
if you want to account for that (and thus forcing you to define def foo(bar: typing.Optional[int] = None)
);
variable number of arguments can't be validated as you would have to define something like def foo(*args: typing.Sequence, **kwargs: typing.Mapping)
and, as said at the beginning, we can only validate containers and not contained objects.
更新
由于这个答案受到了一定的关注,并且受到了一个受其启发的库的发布,需要解决上述提到的缺点。因此,我对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._SpecialForm
作为参数的typing
提示的情况,例如typing.Optional[typing.List[str]]
或typing.Final[typing.Union[typing.Sequence, typing.Mapping]]
。由于这些typing._SpecialForm
的__args__
始终是元组,因此可以递归地找到该元组中包含的提示的__origin__
。结合上述检查,我们将需要过滤掉任何剩余的typing._SpecialForm
。
建议改进:
import inspect
import typing
from functools import wraps
def _find_type_origin(type_hint):
if isinstance(type_hint, typing._SpecialForm):
return
actual_type = typing.get_origin(type_hint) or type_hint
if isinstance(actual_type, typing._SpecialForm):
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帮助我改进了这个答案。
ty.List.__origin__
,它给出了<class 'list'>
。虽然这不能让我检查内部类型,但至少不会再崩溃了。 - Arnestruct.a_str_list[24] = 1
-- 你就无从知晓了。你得编写一个特定的子类来检查其项目,只允许该类而不是list
在你的结构中。这会带来很多运行时开销,更容易通过在API层使用保护程序和类型注释进行预防,在其他地方进行代码风格检查。 - Dunes__origin__
的“绷带”让我可以使用typing
。 - Arnefrom __future__ import annotations
。 - Greg0ry