Проверка подробных типов в 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
что это '.
1 ответов
вместо проверки равенства типов следует использовать isinstance
. Но вы не можете использовать параметризованный универсальный тип (typing.List[int]
) для этого необходимо использовать "универсальную" версию (typing.List
). Таким образом, Вы сможете проверить тип контейнера, но не содержащиеся типы. Параметризованные универсальные типы определяют __origin__
атрибут, который вы можете использовать для этого.
вопреки питон 3.6, 3.7 самое в Python тип подсказок полезным . Сравнить:
# 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
Appart от проверки некоторых подсказок типа, как предложено в предыдущем разделе, этот подход по-прежнему имеет некоторые значения по умолчанию:
- тип подсказки с помощью строк (
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 это помогло мне улучшить этот ответ.