Python 3.7が近づいています で、いくつかの派手な新しいdataclass
+ typing機能をテストしたかったのです。ネイティブの型と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 :)
しかし、私が試したいもう1つのことは、実行時に型ヒントを条件として強制することでした。つまり、誤った型の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
のすべての要素のタイプのチェックを含まないものが望ましい] _ '属性。
型の等価性をチェックする代わりに、isinstance
を使用する必要があります。ただし、パラメータ化されたジェネリック型(_typing.List[int]
_)を使用してこれを行うことはできません。「ジェネリック」バージョン(_typing.List
_)を使用する必要があります。そのため、コンテナタイプは確認できますが、含まれているタイプは確認できません。パラメータ化されたジェネリック型は、そのために使用できる___Origin__
_属性を定義します。
Python 3.6、in 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 に感謝します。
この質問を見つけました。
pydantic は、そのままでデータクラスの完全な型検証を行うことができます。 (入場:私はpydanticを構築しました)
Pydanticのデコレータのバージョンを使用するだけで、結果のデータクラスは完全にVanillaになります。
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)