Python 3.7 est au coin , et je voulais tester certaines des nouvelles fonctionnalités de frappe dataclass
+. Obtenir des conseils pour bien fonctionner est assez facile, avec les types natifs et ceux du module 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 :)
Mais une autre chose que je voulais essayer était de forcer les indications de type comme conditions pendant l'exécution, c'est-à-dire qu'il ne devrait pas être possible pour un dataclass
avec des types incorrects d'exister. Il peut être bien implémenté avec __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')
Ce type de fonction validate
fonctionne pour les types natifs et les classes personnalisées, mais pas celles spécifiées par le module 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
Existe-t-il une meilleure approche pour valider une liste non typée avec une liste typée typing
-? De préférence, qui n'inclut pas la vérification des types de tous les éléments dans un list
, dict
, Tuple
ou set
qui est un dataclass
'attribut.
Au lieu de vérifier l'égalité de type, vous devez utiliser isinstance
. Mais vous ne pouvez pas utiliser un type générique paramétré (typing.List[int]
) Pour cela, vous devez utiliser la version "générique" (typing.List
). Ainsi, vous pourrez vérifier le type de conteneur mais pas les types contenus. Les types génériques paramétrés définissent un attribut __Origin__
Que vous pouvez utiliser pour cela.
Contrairement à Python 3.6, dans Python 3.7 la plupart des indices de type ont un attribut __Origin__
Utile. Comparez:
# Python 3.6
>>> import typing
>>> typing.List.__Origin__
>>> typing.List[int].__Origin__
typing.List
et
# Python 3.7
>>> import typing
>>> typing.List.__Origin__
<class 'list'>
>>> typing.List[int].__Origin__
<class 'list'>
Les exceptions notables étant typing.Any
, typing.Union
Et typing.ClassVar
… Eh bien, tout ce qui est un typing._SpecialForm
Ne définit pas __Origin__
. Heureusement:
>>> isinstance(typing.Union, typing._SpecialForm)
True
>>> isinstance(typing.Union[int, str], typing._SpecialForm)
False
>>> typing.Union[int, str].__Origin__
typing.Union
Mais les types paramétrés définissent un attribut __args__
Qui stocke leurs paramètres en tant que Tuple:
>>> typing.Union[int, str].__args__
(<class 'int'>, <class 'str'>)
Nous pouvons donc améliorer un peu la vérification de type:
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
Ce n'est pas parfait car il ne tiendra pas compte de typing.ClassVar[typing.Union[int, str]]
Ou typing.Optional[typing.List[int]]
Par exemple, mais il devrait faire avancer les choses.
Ensuite, vous pouvez appliquer cette vérification.
Au lieu d'utiliser __post_init__
, J'irais dans la voie du décorateur: cela pourrait être utilisé sur n'importe quoi avec des indices de type, pas seulement 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)
Utilisation étant:
@enforce_types
@dataclasses.dataclass
class Point:
x: float
y: float
@enforce_types
def foo(bar: typing.Union[int, str]):
pass
Hormis la validation de certains indices de type, comme suggéré dans la section précédente, cette approche présente encore certains inconvénients:
class Foo: def __init__(self: 'Foo'): pass
) ne sont pas prises en compte par inspect.getfullargspec
: vous pouvez utiliser typing.get_type_hints
et inspect.signature
à la place;une valeur par défaut qui n'est pas du type approprié n'est pas validée:
@enforce_type
def foo(bar: int = None):
pass
foo()
ne soulève pas de TypeError
. Vous voudrez peut-être utiliser inspect.Signature.bind
en conjonction avec inspect.BoundArguments.apply_defaults
si vous voulez en tenir compte (et donc vous forcer à définir def foo(bar: typing.Optional[int] = None)
);
def foo(*args: typing.Sequence, **kwargs: typing.Mapping)
et, comme dit au début, nous ne pouvons valider que les conteneurs et non les objets contenus.Merci à @ Aran-Fey qui m'a aidé à améliorer cette réponse.
Je viens de trouver cette question.
pydantic peut faire une validation de type complète pour les classes de données hors de la boîte. (admission: j'ai construit pydantic)
Utilisez simplement la version pydantic du décorateur, la classe de données résultante est complètement 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')
La dernière ligne donnera:
...
pydantic.error_wrappers.ValidationError: 1 validation error
id
value is not a valid integer (type=type_error.integer)