10b. Data Models et Métadonnées
dataclassesavancé,field()/metadata,namedtuple,TypedDict, protocole des descripteurs,__slots__avancé,__init_subclass__,total_ordering.
10b.1 dataclasses — toutes les options
Vue en leçon 10, voici tous les paramètres :
from dataclasses import dataclass, field, InitVar, asdict, astuple, replace, fields
@dataclass(
init=True, # génère __init__
repr=True, # génère __repr__
eq=True, # génère __eq__
order=False, # génère __lt__, __le__, __gt__, __ge__
frozen=False, # rend l'instance immuable (__setattr__ bloqué)
slots=False, # génère __slots__ (Python 3.10+)
kw_only=False, # tous les champs deviennent keyword-only
match_args=True, # génère __match_args__ pour pattern matching
)
class Point:
x: float
y: floatfield() — contrôle fin par champ
@dataclass
class Article:
titre: str
auteur: str = field(default="Anonyme")
tags: list[str] = field(default_factory=list) # mutable → factory
id: int = field(init=False) # pas dans __init__
score: float = field(repr=False) # caché dans __repr__
_cache: dict = field(repr=False, compare=False, hash=False)
def __post_init__(self):
self.id = hash(self.titre) # initialisé après __init__metadata — stocker des métadonnées arbitraires (accessibles via fields()) :
from dataclasses import field, fields
@dataclass
class Utilisateur:
nom: str = field(metadata={"label": "Nom complet", "ordre": 1})
âge: int = field(metadata={"label": "Âge", "ordre": 2, "min": 0, "max": 150})
# Lecture des métadonnées
for f in fields(Utilisateur):
print(f.name, f.metadata["label"])Utile pour : sérialisation automatique, génération de formulaire, validation.
InitVar — variables d’initialisation seules
Passées à __post_init__ mais pas stockées comme champ :
@dataclass
class Compte:
nom: str
solde: float
taux: InitVar[float] = 0.02 # pas un champ de l'instance
def __post_init__(self, taux: float):
self.solde *= 1 + taux__post_init__ — hook d’initialisation
@dataclass
class Intervalle:
début: float
fin: float
def __post_init__(self):
if self.début > self.fin:
raise ValueError(f"début > fin: {self.début} > {self.fin}")Héritage avec dataclasses
@dataclass
class Base:
x: int = 0
@dataclass
class Dérivée(Base):
y: int = 0
# x et y sont tous deux dans __init__Attention : les champs avec valeurs par défaut doivent venir après ceux sans dans l’ordre MRO.
asdict, astuple, replace, fields
p = Point(1.0, 2.0)
asdict(p) # {'x': 1.0, 'y': 2.0}
astuple(p) # (1.0, 2.0)
replace(p, x=5.0) # Point(x=5.0, y=2.0) — crée une copie modifiée
for f in fields(Point):
print(f.name, f.type, f.default)frozen=True et héritage
@dataclass(frozen=True)
class Point:
x: float
y: float
p = Point(1, 2)
# p.x = 3 # FrozenInstanceError
@dataclass(frozen=True)
class PointColoré(Point):
couleur: strImpossible de modifier, mais replace() contourne en créant une nouvelle instance.
10b.2 Alternatives aux dataclasses
namedtuple
Tuple avec champs nommés (immuable, léger) :
from collections import namedtuple
Point = namedtuple("Point", ["x", "y"])
p = Point(1, 2)
p.x # 1
p[0] # 1 (c'est aussi un tuple)
x, y = p # unpackingMéthodes :
p._asdict() # {'x': 1, 'y': 2}
p._replace(x=5) # Point(x=5, y=2)
p._fields # ('x', 'y')
Point._make([3, 4]) # Point(x=3, y=4)Héritage (déconseillé, préférer dataclass) :
class Point3D(Point):
__slots__ = ()
def __new__(cls, x, y, z):
return super().__new__(cls, x, y)TypedDict — dictionnaire typé (PEP 589)
Utile pour des données structurées type JSON :
from typing import TypedDict
class Utilisateur(TypedDict):
nom: str
âge: int
email: str
u: Utilisateur = {"nom": "Alice", "âge": 30, "email": "alice@ex.com"}
# u["inconnu"] # TypeError à la compilation (mypy) mais pas au runtimetotal=False : clés optionnelles :
class Config(TypedDict, total=False):
debug: bool
timeout: floatHéritage entre TypedDict, required/not_required (3.11+).
SimpleNamespace
Objet mutable avec accès par attribut :
from types import SimpleNamespace
cfg = SimpleNamespace(debug=True, port=8080)
cfg.debug # True
cfg.host = "localhost" # ajout dynamiqueIdéal pour configuration rapide, moins formel que dataclass.
10b.3 Descripteurs (protocole)
Un descripteur est un objet qui implémente __get__, __set__, ou __delete__.
C’est le mécanisme sous @property, @staticmethod, @classmethod, __slots__.
class Descripteur:
def __get__(self, obj, objtype=None):
print(f"get {obj=}")
return obj._valeur
def __set__(self, obj, valeur):
print(f"set {valeur=}")
obj._valeur = valeur
def __delete__(self, obj):
print("delete")
del obj._valeur
class MaClasse:
attr = Descripteur() # descripteur au niveau classe
obj = MaClasse()
obj.attr = 42 # appelle __set__
print(obj.attr) # appelle __get__
del obj.attr # appelle __delete__@property — descripteur intégré
@property
def x(self):
return self._x
# Équivaut à :
x = property(fget=lambda self: self._x, fset=..., fdel=..., doc="...")@staticmethod et @classmethod — descripteurs aussi
class MaClasse:
@staticmethod
def f(): pass
@classmethod
def g(cls): pass
# Sont des descripteurs qui modifient le binding :
# staticmethod → pas de self/cls passé
# classmethod → cls passé au lieu de self__set_name__ — connaître le nom de l’attribut (3.6+)
class Validé:
def __set_name__(self, propriétaire, nom):
self.nom = f"_{nom}"
def __get__(self, obj, objtype=None):
return getattr(obj, self.nom)
def __set__(self, obj, valeur):
if valeur < 0:
raise ValueError("Valeur négative interdite")
setattr(obj, self.nom, valeur)
class Point:
x = Validé()
y = Validé()
def __init__(self, x, y):
self.x = x
self.y = yDescripteurs vs @property — quand utiliser ?
@property | Descripteur personnalisé |
|---|---|
| Usage unique | Réutilisable sur plusieurs classes |
| Simple getter/setter | Logique complexe, validation, transformation |
| Pas de paramètres | Peut accepter des paramètres via __init__ |
10b.4 __slots__ avancé
class Point:
__slots__ = ("x", "y")
# Pas de __dict__ → mémoire réduite, accès plus rapide
def __init__(self, x, y):
self.x = x
self.y = y
# p = Point(1, 2)
# p.z = 3 # AttributeErrorHéritage : une classe avec __slots__ et une sous-classe sans __slots__ aura __dict__.
class PointBase:
__slots__ = ("x", "y")
class Point3D(PointBase):
pass # a __dict__ + les slots du parent
# Solution : redéclarer __slots__
class Point3D(PointBase):
__slots__ = ("z",)Ajouter __dict__ dans __slots__ pour garder la flexibilité :
class Point:
__slots__ = ("x", "y", "__dict__") # slots + dict10b.5 @total_ordering
Génère tous les comparateurs à partir d’un seul :
from functools import total_ordering
@total_ordering
class Note:
def __init__(self, valeur: int):
self.valeur = valeur
def __eq__(self, other):
return self.valeur == other.valeur
def __lt__(self, other):
return self.valeur < other.valeur
# __le__, __gt__, __ge__ auto-générés10b.6 __init_subclass__ (3.6+)
Hook appelé quand une sous-classe est créée :
class PluginBase:
registry = {}
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
PluginBase.registry[cls.__name__] = cls
class PluginA(PluginBase): pass
class PluginB(PluginBase): pass
print(PluginBase.registry)
# {'PluginA': <class PluginA>, 'PluginB': <class PluginB>}10b.7 Métadonnées de fonction : __annotations__
def f(x: int, y: str) -> bool:
return True
f.__annotations__ # {'x': int, 'y': str, 'return': bool}Accessible depuis dataclasses.fields() et inspect.signature().
10b.8 Tableau récapitulatif
| Outil | Mutable ? | Typé ? | Accès | Cas d’usage |
|---|---|---|---|---|
dict | Oui | Non statique | d["key"] | Données dynamiques, JSON |
TypedDict | Oui | Oui (mypy) | d["key"] | Données structurées type JSON |
namedtuple | Non | Non | .attr / [idx] | Petit immutable nommé |
dataclass | Oui/non (frozen) | Oui | .attr | Classe de données complète |
SimpleNamespace | Oui | Non | .attr | Configuration rapide |
| Descripteur | Selon impl. | Oui | .attr | Validation/réutilisation |