10b. Data Models et Métadonnées

dataclasses avancé, 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: float

field() — 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: str

Impossible 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         # unpacking

Mé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 runtime

total=False : clés optionnelles :

class Config(TypedDict, total=False):
    debug: bool
    timeout: float

Hé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 dynamique

Idé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 = y

Descripteurs vs @property — quand utiliser ?

@propertyDescripteur personnalisé
Usage uniqueRéutilisable sur plusieurs classes
Simple getter/setterLogique complexe, validation, transformation
Pas de paramètresPeut 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  # AttributeError

Hé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 + dict

10b.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és

10b.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

OutilMutable ?Typé ?AccèsCas d’usage
dictOuiNon statiqued["key"]Données dynamiques, JSON
TypedDictOuiOui (mypy)d["key"]Données structurées type JSON
namedtupleNonNon.attr / [idx]Petit immutable nommé
dataclassOui/non (frozen)Oui.attrClasse de données complète
SimpleNamespaceOuiNon.attrConfiguration rapide
DescripteurSelon impl.Oui.attrValidation/réutilisation

🔗 ← Retour au cours · ← précédent · Suivant →