W3docs

Les Dataclasses Python

Apprenez les dataclasses Python : le décorateur @dataclass, les valeurs par défaut avec field(), l'ordre, l'immuabilité et l'héritage avec des exemples concrets.

Une dataclass est une classe Python ordinaire dont le code répétitif — __init__, __repr__ et __eq__ — est généré automatiquement par le décorateur @dataclass. Le résultat est moins de code, moins d'erreurs, et des classes qui sont immédiatement lisibles.

Ce chapitre couvre :

  • Pourquoi les dataclasses existent et quand les utiliser
  • Le décorateur @dataclass
  • Les valeurs par défaut des champs et le helper field()
  • Contrôle de l'égalité et de l'ordre
  • Les dataclasses immuables avec frozen=True
  • La logique de post-initialisation avec __post_init__
  • L'héritage avec les dataclasses
  • Dataclasses vs. NamedTuple vs. classes ordinaires

Avant de lire ce chapitre, assurez-vous d'être à l'aise avec les classes et objets Python et l'héritage Python.

Pourquoi les Dataclasses ?

Considérez une classe qui stocke un produit dans une boutique en ligne. Sans dataclasses, vous écrivez les mêmes affectations d'attributs trois fois — une fois dans __init__, une fois dans __repr__, et une fois dans __eq__ :

class Product:
    def __init__(self, name, price, stock):
        self.name = name
        self.price = price
        self.stock = stock

    def __repr__(self):
        return f"Product(name={self.name!r}, price={self.price}, stock={self.stock})"

    def __eq__(self, other):
        if not isinstance(other, Product):
            return NotImplemented
        return (self.name, self.price, self.stock) == (other.name, other.price, other.stock)

Le décorateur @dataclass génère tout ce qui précède à partir d'une seule liste annotée de champs :

from dataclasses import dataclass

@dataclass
class Product:
    name: str
    price: float
    stock: int

Les deux versions se comportent de façon identique. La version dataclass est plus courte, moins sujette aux erreurs, et communique instantanément que cette classe est avant tout un conteneur de données.

Le Décorateur @dataclass

Importez dataclass depuis le module standard dataclasses et appliquez-le à votre classe. Chaque champ est déclaré comme une variable de classe avec annotation de type :

from dataclasses import dataclass

@dataclass
class Point:
    x: float
    y: float

p = Point(1.5, 2.0)
print(p)          # Point(x=1.5, y=2.0)
print(p.x)        # 1.5

p2 = Point(1.5, 2.0)
print(p == p2)    # True  — __eq__ compares field by field

Le décorateur génère :

MéthodeCe qu'elle fait
__init__Accepte chaque champ comme paramètre et l'assigne à self
__repr__Retourne une chaîne lisible comme Point(x=1.5, y=2.0)
__eq__Compare deux instances champ par champ

Les annotations de type sont obligatoires mais pas vérifiées à l'exécution

Les déclarations de champ nécessitent une annotation de type (x: float). Python ne vérifie pas le type à l'exécution — vous pouvez toujours passer une chaîne là où un float est attendu. L'annotation est une métadonnée utilisée par les vérificateurs de types tels que mypy et par le mécanisme dataclasses lui-même. Pour la validation de type à l'exécution, voir Python Type Hints.

Valeurs par Défaut

Assignez une valeur par défaut directement sur le champ pour le rendre optionnel dans __init__ :

from dataclasses import dataclass

@dataclass
class Config:
    host: str = "localhost"
    port: int = 8080
    debug: bool = False

c1 = Config()
print(c1)   # Config(host='localhost', port=8080, debug=False)

c2 = Config(host="example.com", port=443)
print(c2)   # Config(host='example.com', port=443, debug=False)

Les champs avec des valeurs par défaut doivent apparaître après les champs sans valeurs par défaut — exactement la même règle que pour les paramètres de fonctions ordinaires.

Valeurs par défaut mutables et field()

Vous ne pouvez pas utiliser un objet mutable (une liste, un dict, ou un ensemble) comme valeur par défaut simple. Python partagerait une seule liste entre toutes les instances, ce qui entraîne des bogues subtils :

from dataclasses import dataclass

# This raises a ValueError at class definition time:
# @dataclass
# class Bag:
#     items: list = []   # ValueError: mutable default is not allowed

Utilisez plutôt field(default_factory=...) pour créer un nouvel objet pour chaque instance :

from dataclasses import dataclass, field

@dataclass
class Bag:
    items: list = field(default_factory=list)

b1 = Bag()
b2 = Bag()
b1.items.append("apple")

print(b1.items)   # ['apple']
print(b2.items)   # []  — b2 has its own separate list

default_factory accepte tout callable sans argument, y compris les lambdas et vos propres fonctions.

Le Helper field()

field() vous donne un contrôle précis sur les champs individuels. Ses paramètres les plus utiles sont :

ParamètreRôle
defaultUne valeur par défaut simple (scalaire uniquement)
default_factoryUn callable qui produit la valeur par défaut
reprFalse pour exclure ce champ de __repr__
compareFalse pour exclure ce champ de __eq__ (et de l'ordre)
initFalse pour exclure ce champ de __init__
from dataclasses import dataclass, field
import time

@dataclass
class LogEntry:
    message: str
    level: str = "INFO"
    timestamp: float = field(default_factory=time.time, repr=False, compare=False)

entry = LogEntry("Server started")
print(entry)               # LogEntry(message='Server started', level='INFO')
# timestamp exists but is hidden from repr and ignored in comparisons
print(entry.timestamp > 0) # True

Ordre

Par défaut, les dataclasses prennent en charge l'égalité (==, !=) mais pas l'ordre (<, >, <=, >=). Activez l'ordre en passant order=True au décorateur :

from dataclasses import dataclass

@dataclass(order=True)
class Version:
    major: int
    minor: int
    patch: int

v1 = Version(1, 2, 0)
v2 = Version(1, 3, 0)
v3 = Version(1, 2, 0)

print(v1 < v2)    # True
print(v1 == v3)   # True
print(v2 > v1)    # True

versions = [Version(2, 0, 0), Version(1, 9, 1), Version(1, 2, 3)]
print(sorted(versions))
# [Version(major=1, minor=2, patch=3),
#  Version(major=1, minor=9, patch=1),
#  Version(major=2, minor=0, patch=0)]

Python génère les méthodes de comparaison en comparant les champs dans l'ordre où ils sont déclarés, à la manière des tuples. Vous pouvez exclure un champ des comparaisons avec field(compare=False).

Dataclasses Immuables avec frozen=True

Passez frozen=True pour rendre tous les champs en lecture seule après la création. Toute tentative de modifier un champ lève une FrozenInstanceError :

from dataclasses import dataclass

@dataclass(frozen=True)
class Coordinate:
    lat: float
    lon: float

london = Coordinate(51.5074, -0.1278)
print(london)        # Coordinate(lat=51.5074, lon=-0.1278)

# london.lat = 0.0  # FrozenInstanceError: cannot assign to field 'lat'

Les dataclasses gelées sont aussi hachables (elles implémentent __hash__), donc vous pouvez les utiliser comme clés de dictionnaire ou membres d'ensemble :

from dataclasses import dataclass

@dataclass(frozen=True)
class Coordinate:
    lat: float
    lon: float

cities = {
    Coordinate(51.5074, -0.1278): "London",
    Coordinate(48.8566,  2.3522): "Paris",
}
print(cities[Coordinate(51.5074, -0.1278)])   # London

Les dataclasses ordinaires (mutables) ne sont pas hachables par défaut — Python met __hash__ à None lorsque __eq__ est défini sans frozen=True.

Logique de Post-Initialisation avec __post_init__

Parfois, vous avez besoin de dériver la valeur d'un champ à partir d'autres champs, ou de valider l'entrée après l'exécution de __init__. Définissez une méthode __post_init__ — elle est appelée automatiquement à la fin du __init__ généré :

from dataclasses import dataclass, field
import math

@dataclass
class Circle:
    radius: float

    def __post_init__(self):
        if self.radius <= 0:
            raise ValueError(f"radius must be positive, got {self.radius}")

    @property
    def area(self):
        return math.pi * self.radius ** 2

c = Circle(5)
print(round(c.area, 4))   # 78.5398

# Circle(-1)  # ValueError: radius must be positive, got -1

Vous pouvez également calculer un champ dérivé. Marquez-le avec field(init=False) pour qu'il n'apparaisse pas dans __init__, puis définissez-le à l'intérieur de __post_init__ :

from dataclasses import dataclass, field

@dataclass
class Rectangle:
    width: float
    height: float
    area: float = field(init=False, repr=True)

    def __post_init__(self):
        self.area = self.width * self.height

r = Rectangle(4, 6)
print(r)         # Rectangle(width=4, height=6, area=24)
print(r.area)    # 24

Héritage avec les Dataclasses

Une dataclass peut hériter d'une autre dataclass. Le __init__ de la classe enfant inclut les champs des deux classes — les champs du parent en premier, dans l'ordre où ils ont été déclarés :

from dataclasses import dataclass

@dataclass
class Animal:
    name: str
    age: int

@dataclass
class Dog(Animal):
    breed: str

rex = Dog(name="Rex", age=3, breed="Labrador")
print(rex)    # Dog(name='Rex', age=3, breed='Labrador')

Point d'attention : si une classe parente a un champ avec une valeur par défaut, tous les champs de la classe enfant doivent également avoir des valeurs par défaut. C'est la même règle qui s'applique aux signatures de fonctions Python ordinaires — un paramètre sans valeur par défaut ne peut pas suivre un paramètre avec valeur par défaut.

from dataclasses import dataclass

@dataclass
class Animal:
    name: str
    age: int = 0   # has a default

# @dataclass
# class Dog(Animal):
#     breed: str   # TypeError: non-default argument 'breed' follows default argument

Contournez cela en donnant également une valeur par défaut au champ de la classe enfant, ou en restructurant la hiérarchie de sorte que les champs avec valeurs par défaut viennent en dernier.

Paramètres du Décorateur en un Coup d'œil

@dataclass(
    init=True,     # generate __init__       (default True)
    repr=True,     # generate __repr__       (default True)
    eq=True,       # generate __eq__         (default True)
    order=False,   # generate <, >, <=, >=   (default False)
    frozen=False,  # make fields immutable   (default False)
)
class MyClass:
    ...

Vous avez rarement besoin de toucher la plupart de ces paramètres. Les plus courants sont order=True et frozen=True.

Fonctions Utilitaires

Le module dataclasses fournit également trois fonctions pratiques :

fields()

Retourne un tuple d'objets Field décrivant chaque champ de la classe :

from dataclasses import dataclass, fields

@dataclass
class Point:
    x: float
    y: float

for f in fields(Point):
    print(f.name, f.type)
# x <class 'float'>
# y <class 'float'>

asdict()

Convertit une instance de dataclass en dictionnaire simple (de manière récursive) :

from dataclasses import dataclass, asdict

@dataclass
class Address:
    street: str
    city: str

@dataclass
class Person:
    name: str
    address: Address

p = Person("Alice", Address("10 Downing St", "London"))
print(asdict(p))
# {'name': 'Alice', 'address': {'street': '10 Downing St', 'city': 'London'}}

Cela est utile lors de la sérialisation vers JSON ou de l'envoi de données à une API.

astuple()

Convertit en tuple (de manière récursive) :

from dataclasses import dataclass, astuple

@dataclass
class Point:
    x: float
    y: float

p = Point(3.0, 4.0)
print(astuple(p))   # (3.0, 4.0)

Dataclasses vs. NamedTuple vs. Classes Ordinaires

FonctionnalitéClasse ordinaireNamedTupledataclass
__init__ automatiqueNonOuiOui
__repr__ automatiqueNonOuiOui
__eq__ automatiqueNonOui (par valeur)Oui (par valeur)
MutableOuiNonOui (par défaut)
HachableNon (si __eq__ défini)OuiUniquement avec frozen=True
OrdreManuelOuiorder=True
HéritageOuiLimitéOui
Vérification isinstanceOuiOui (aussi tuple)Oui
Déstructuration (a, b = obj)NonOuiNon

Utilisez une dataclass quand :

  • Vous voulez des données mutables avec une immuabilité optionnelle.
  • Vous avez besoin de l'héritage ou d'une logique post-init.
  • Vous voulez un contrôle précis des champs (field()).

Utilisez NamedTuple quand :

  • Vous voulez un enregistrement immuable qui se comporte également comme un tuple (déstructuration positionnelle, lignes CSV).
  • Vous avez besoin de compatibilité avec du code qui attend des tuples.

Utilisez une classe ordinaire quand :

  • La classe a un comportement significatif et très peu de données brutes.
  • Vous avez besoin d'un __init__ personnalisé qui ne peut pas être exprimé via __post_init__.

Pièges Courants

Valeurs par défaut mutables. Utiliser une liste ou un dict comme valeur par défaut simple lève une ValueError au moment de la définition de la classe. Utilisez toujours field(default_factory=...).

Hachage. Les dataclasses ordinaires ne sont pas hachables. Si vous avez besoin de les utiliser comme clés de dict ou dans des ensembles, utilisez frozen=True ou passez unsafe_hash=True (rarement recommandé).

eq=False. Si vous désactivez la génération de l'égalité (eq=False), Python retombe sur la comparaison d'identité (is), ce qui n'est presque jamais ce que vous voulez pour des objets de données.

Ordre des valeurs par défaut hérités. Si un champ parent a une valeur par défaut et qu'un champ enfant n'en a pas, Python lève une TypeError. Planifiez soigneusement l'ordre des champs dans votre hiérarchie.

Résumé

ConceptCe qu'il fait
@dataclassGénère __init__, __repr__, __eq__ automatiquement
field()Contrôle précis des champs : valeurs par défaut, repr, compare, init
default_factoryFournit une valeur par défaut mutable fraîche pour chaque instance
order=TrueAjoute <, >, <=, >= basé sur l'ordre des champs
frozen=TrueRend les champs en lecture seule et l'instance hachable
__post_init__S'exécute après __init__ pour la validation ou les champs dérivés
fields()Retourne les métadonnées sur chaque champ
asdict()Convertit l'instance en dict simple (de manière récursive)
astuple()Convertit l'instance en tuple simple (de manière récursive)

Pour les sujets connexes, voir les classes et objets Python, l'héritage Python, et les classes de base abstraites Python.

Pratique

Pratique
Which decorator do you use to create a dataclass in Python?
Which decorator do you use to create a dataclass in Python?
Was this page helpful?