Вместо проверки на равенство типов следует использовать 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'>
Известные исключения: typing.Any
, typing.Union
и typing.ClassVar
... Ну, все, что является typing._SpecialForm
, не определяет __origin__
.К счастью:
>>> isinstance(typing.Union, typing._SpecialForm)
True
>>> isinstance(typing.Union[int, str], typing._SpecialForm)
False
>>> typing.Union[int, str].__origin__
typing.Union
Но параметризованные типы определяют атрибут __args__
, который хранит свои параметры в виде кортежа:
>>> typing.Union[int, str].__args__
(<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:
actual_type = 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:
actual_type = 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)
, и, как было сказано в начале, мы можем проверять только контейнеры и не содержащие объекты.
Благодаря @ Aran-Fey , который помог мне улучшить этот ответ.