Introduites avec la PEP 557 et Python 3.7, les dataclasses simplifient la création de classes. En effet, elles sont très utilisées pour les classes dédiées au stockage de données, avec très peu de code.
Par exemple, les dataclasses automatisent la génération de méthodes __init__, __repr__ et __eq__.
Nous allons voir les différents avantages qu'apportent les dataclasses :
-
Simplicité de déclaration
-
Lisibilité du code
-
Réduction du code
-
Flexibilité et personnalisation
Qu'est-ce qu'une dataclass ?
Avec une classe standard, il est nécessaire d'implémenter le constructeur ainsi que d'autres méthodes pour définir les attributs et les afficher.
Les dataclasses permettent de se concentrer sur la définition des attributs tout en laissant Python gérer beaucoup de choses pour nous. Ainsi le code est beaucoup plus concis et lisible.
Prenons un exemple, la définition d'une classe Voiture :
class Voiture: def __init__(self, marque, modele, annee, couleur, kilometrage): self.marque = marque self.modele = modele self.annee = annee self.couleur = couleur self.kilometrage = kilometrage def __repr__(self): return f"Voiture(marque='{self.marque}', modele='{self.modele}', annee={self.annee}, couleur='{self.couleur}', kilometrage={self.kilometrage})" def __eq__(self, other): if not isinstance(other, Voiture): return False return (self.marque == other.marque and self.modele == other.modele and self.annee == other.annee and self.couleur == other.couleur and self.kilometrage == other.kilometrage)
Pour faire la même chose avec les dataclasses :
from dataclasses import dataclass @dataclass class Voiture: marque: str modele: str annee: int couleur: str kilometrage: int
La version avec dataclass est donc beaucoup plus concise.
Syntaxe de base d'une dataclass
Pour déclarer une dataclass, il suffit d'ajouter le décorateur @dataclass à la déclaration de la classe. De plus, chaque attribut doit être annoté avec un type.
Voici un exemple simple :
from dataclasses import dataclass @dataclass class Personne: nom: str age: int
Votre classe est prête à être instanciée. Toujours avec ce même exemple, on remarque que __repr__ est automatiquement générée :
from dataclasses import dataclass @dataclass class Personne: nom: str age: int personne = Personne(nom="Patrick", age=30) print(repr(personne)) # Affiche Personne(nom='Patrick', age=30)
Paramètres du décorateur @dataclass
Les dataclasses sont personnalisables via plusieurs paramètres qui permettent d'ajuster le comportement des dataclasses.
Voici quelques paramètres avec des exemples :
-
initgénère automatiquement la méthode__init__
@dataclass(init=False), en définissantinitàFalse, il faut implémenter la méthode__init__manuellement. -
reprgénère la méthode__repr__ -
eqgénère la méthode__eq__qui permet de comparer les instances entre elles (via leurs attributs) -
orderpar défaut défini àFalse, permet d'ordonner les instances en ajoutant les méthodes__lt__,__le__,__gt__et__ge__
from dataclasses import dataclass @dataclass(order=True) class Student: grade: float name: str age: int # Création de quelques étudiants student1 = Student(15.5, "Patrick", 20) student2 = Student(14.0, "Robert", 21) student3 = Student(15.5, "Ely", 19) student4 = Student(9.0, "Damien", 22) # Maintenant on peut comparer et trier les étudiants # Par défaut, la comparaison se fait sur tous les champs dans l'ordre de déclaration print(student1 > student2) # True (car 15.5 > 14.0) print(student1 == student3) # False car les noms sont différents # On peut aussi trier une liste d'étudiants students = [student2, student3, student1, student4] sorted_students = sorted(students) for student in sorted_students: print(f"{student.name}: {student.grade}") """ Damien: 9.0 Robert: 14.0 Ely: 15.5 Patrick: 15.5 """
frozenpermet de rendre l'instance immuable après sa création
from dataclasses import dataclass @dataclass(frozen=True) class FrozenClass: name: str age: int gab = FrozenClass(name="Gab", age=30) gab.age = 35 # Lève dataclasses.FrozenInstanceError
Nous avons vu les principaux paramètres qui permettent une personnalisation fine de nos classes, en ne générant que les méthodes nécessaires.
Les champs d'une dataclass
Précédemment, nous avons vu que chaque attribut doit être annoté avec son type. De plus, il est possible de personnaliser une dataclass via ses attributs.
Valeur par défaut
Avec les dataclasses, il est possible d'assigner une valeur par défaut :
from dataclasses import dataclass @dataclass class Personne: nom: str age: int = 30 p = Personne("Dupont") print(p.nom) # Affiche "Dupont" print(p.age) # Affiche 30
La fonction field
Pour affiner davantage le comportement d'une dataclass, on peut utiliser la fonction field. Cette dernière accepte plusieurs paramètres.
from dataclasses import dataclass, field @dataclass class Vehicule: modele: str marque: str places: int = field(default=5, repr=False) v = Vehicule("Model 3", "Tesla") print(repr(v)) # Vehicule(modele='Model 3', marque='Tesla')
Dans cet exemple, le champ places a une valeur par défaut et n'apparaît pas avec __repr__.
default_factory est un autre paramètre essentiel pour travailler avec des objets mutables. Il faut lui passer un objet callable.
Python lèvera une erreur si vous définissez une liste en valeur par défaut :
from dataclasses import dataclass from typing import List @dataclass class MaCLasse: liste: List[int] = [1, 2, 3] # Raise ValueError: mutable default <class 'list'> for field liste is not allowed: use default_factory
Une sécurité importante qui permet d'empêcher que toutes les instances partagent la même liste. Pour ne pas avoir d'erreur, il faut penser à utiliser le paramètre default_factory :
from dataclasses import dataclass, field from typing import List @dataclass class MaCLasse: liste: List[int] = field(default_factory=list)
Personnaliser l'initialisation avec __post_init__
La méthode __post_init__ est automatiquement appelée après la méthode __init__. Elle permet d'effectuer des opérations supplémentaires comme :
-
la validation de données
-
le formatage de données
-
le calcul de champs dérivés
Voici quelques exemples :
from dataclasses import dataclass @dataclass class Email: address: str priority: str = "normal" def __post_init__(self): # Validation simple du format email if '@' not in self.address or '.' not in self.address: raise ValueError("Format d'email invalide") # Exemples d'utilisation email1 = Email("[email protected]", "HIGH") print(email1) # Email(address='[email protected]', priority='high') email2 = Email("invalid") # Lève ValueError: Format d'email invalide
from dataclasses import dataclass, field @dataclass class Rectangle: width: float height: float area: float = field(init=False) # champ calculé, non inclus dans l'init def __post_init__(self): self.area = self.width * self.height # calcul automatique après initialisation # Exemple d'utilisation rectangle = Rectangle(width=5, height=3) print(rectangle.area) # Affiche: 15