cours
Classes de données Python : Un tutoriel complet
Les classes de données sont l'une des caractéristiques de Python qui, une fois que vous les avez découvertes, ne vous permet plus de revenir à l'ancienne méthode. Considérez cette classe régulière :
class Exercise:
def __init__(self, name, reps, sets, weight):
self.name = name
self.reps = reps
self.sets = sets
self.weight = weight
Pour moi, cette définition de classe est très inefficace - dans la méthode __init__
, vous répétez chaque paramètre au moins trois fois. Cela peut sembler anodin, mais pensez au nombre de fois où vous écrivez des classes avec beaucoup plus de paramètres.
En comparaison, jetez un coup d'œil à l'alternative des classes de données du code ci-dessus :
from dataclasses import dataclass
@dataclass
class Exercise:
name: str
reps: int
sets: int
weight: float # Weight in lbs
Ce morceau de code d'apparence modeste est bien meilleur qu'une classe ordinaire. Le minuscule décorateur @dataclass
implémente les classes __init__
, __repr__
, __eq__
dans les coulisses, ce qui aurait nécessité au moins 20 lignes de code manuellement.
En outre, de nombreuses autres fonctionnalités, telles que les opérateurs de comparaison, l'ordonnancement des objets et l'immutabilité, sont toutes à une ligne près d'être créées comme par magie pour notre classe.
Le but de ce tutoriel est donc de vous montrer pourquoi les classes de données sont l'une des meilleures choses qui soient arrivées à Python si vous aimez la programmation orientée objet.
Commençons !
Les bases des classes de données Python
Abordons quelques-uns des concepts fondamentaux des classes de données Python qui les rendent si utiles.
Certaines méthodes sont automatiquement générées dans les classes de données
Malgré toutes leurs caractéristiques, les classes de données sont des classes normales qui nécessitent beaucoup moins de code pour mettre en œuvre la même fonctionnalité. Voici à nouveau la classe Exercise
:
from dataclasses import dataclass
@dataclass
class Exercise:
name: str
reps: int
sets: int
weight: float
ex1 = Exercise("Bench press", 10, 3, 52.5)
# Verifying Exercise is a regular class
ex1.name
'Bench press'
À l'heure actuelle, Exercise
dispose déjà des méthodes __repr__
et __eq__
. Vérifions-le :
repr(ex1)
"Exercise(name='Bench press', reps=10, sets=3, weight=52.5)"
La représentation d'un objet repr
doit renvoyer le code qui peut se recréer lui-même, et nous pouvons voir que c'est exactement le cas pour ex1
.
En comparaison, Exercise
défini selon l'ancienne méthode ressemblerait à ceci :
class Exercise:
def __init__(self, name, reps, sets, weight):
self.name = name
self.reps = reps
self.sets = sets
self.weight = weight
ex3 = Exercise("Bench press", 10, 3, 52.5)
ex3
<__main__.Exercise at 0x7f6834100130>
Il a l'air assez horrible et inutile !
Vérifions maintenant l'existence de __eq__
, qui est l'opérateur d'égalité :
# Redefine the class
@dataclass
class Exercise:
name: str
reps: int
sets: int
weight: float
ex1 = Exercise("Bench press", 10, 3, 52.5)
ex2 = Exercise("Bench press", 10, 3, 52.5)
La comparaison de la classe à elle-même et à une autre classe avec des paramètres identiques doit renvoyer True :
ex1 == ex2
True
ex1 == ex1
True
Et c'est le cas ! Dans les classes ordinaires, cette logique aurait été pénible à écrire.
Les classes de données nécessitent des indications de type
Comme vous l'avez peut-être remarqué, les classes de données nécessitent des indications de type lors de la définition des champs. En fait, les classes de données autorisent n'importe quel type du module typing
. Par exemple, voici comment créer un champ qui peut accepter le type de données Any
:
from typing import Any
@dataclass
class Dummy:
attr: Any
Cependant, l'idiosyncrasie de Python est que même si les classes de données nécessitent des indications de type, les types ne sont pas réellement imposés.
Par exemple, la création d'une instance de la classe Exercise
avec des types de données totalement incorrects peut être exécutée sans erreur :
silly_exercise = Exercise("Bench press", "ten", "three sets", 52.5)
silly_exercise.sets
“Three sets”
Si vous souhaitez renforcer les types de données, vous devez utiliser des vérificateurs de types tels que Mypy.
Les classes de données autorisent les valeurs par défaut dans les champs
Jusqu'à présent, nous n'avons pas ajouté de valeurs par défaut à nos classes. Corrigeons cela :
@dataclass
class Exercise:
name: str = "Push-ups"
reps: int = 10
sets: int = 3
weight: float = 0
# Now, all fields have defaults
ex5 = Exercise()
ex5
Exercise(name='Push-ups', reps=10, sets=3, weight=0)
N'oubliez pas que les champs qui ne sont pas des champs par défaut ne peuvent pas suivre les champs par défaut. Par exemple, le code ci-dessous provoquera une erreur :
@dataclass
class Exercise:
name: str = "Push-ups"
reps: int = 10
sets: int = 3
weight: float # NOT ALLOWED
ex5 = Exercise()
ex5
TypeError: non-default argument 'weight' follows default argument
Dans la pratique, vous définirez rarement des valeurs par défaut à l'aide de la syntaxe name: type = value
.
Vous utiliserez plutôt la fonction field
, qui permet de mieux contrôler chaque définition de champ :
from dataclasses import field
@dataclass
class Exercise:
name: str = field(default="Push-up")
reps: int = field(default=10)
sets: int = field(default=3)
weight: float = field(default=0)
# Now, all fields have defaults
ex5 = Exercise()
ex5
Exercise(name='Push-up', reps=10, sets=3, weight=0)
La fonction field
a plus de paramètres, tels que
repr
init
compare
default_factory
et ainsi de suite. Nous les examinerons dans les sections suivantes.
Les classes de données peuvent être créées à l'aide d'une fonction
Une dernière remarque sur les bases des classes de données est que leur définition peut être encore plus courte en utilisant la fonction make_dataclass
:
from dataclasses import make_dataclass
Exercise = make_dataclass(
"Exercise",
[
("name", str),
("reps", int),
("sets", int),
("weight", float),
],
)
ex3 = Exercise("Deadlifts", 8, 3, 69.0)
ex3
Exercise(name='Deadlifts', reps=8, sets=3, weight=69.0)
Mais vous sacrifierez la lisibilité, c'est pourquoi je ne recommande pas l'utilisation de cette fonction.
Classes de données avancées en Python
Dans cette section, nous aborderons les fonctionnalités avancées des classes de données qui apportent davantage d'avantages. L'une de ces caractéristiques est une usine par défaut.
Usines par défaut
Pour expliquer les usines par défaut, créons une autre classe nommée WorkoutSession
qui accepte deux champs :
from dataclasses import dataclass
from typing import List
@dataclass
class Exercise:
name: str = "Push-ups"
reps: int = 10
sets: int = 3
weight: float = 0
@dataclass
class WorkoutSession:
exercises: List[Exercise]
duration_minutes: int
En utilisant le type List
, nous spécifions que WorkoutSession
accepte une liste d'instances Exercise
.
# Define the Exercise instances for HIIT training
ex1 = Exercise(name="Burpees", reps=15, sets=3)
ex2 = Exercise(name="Mountain Climbers", reps=20, sets=3)
ex3 = Exercise(name="Jump Squats", reps=12, sets=3)
exercises_monday = [ex1, ex2, ex3]
hiit_monday = WorkoutSession(exercises=exercises_monday, duration_minutes=30)
Actuellement, chaque instance de séance d'entraînement nécessite l'initialisation des exercices. Mais cela ne reflète pas la façon dont les gens s'entraînent - ils commencent d'abord une session (probablement dans une application), puis ils ajoutent des exercices au fur et à mesure qu'ils s'entraînent.
Il faut donc pouvoir créer des sessions sans exercices et sans durée. Pour ce faire, ajoutons une liste vide comme valeur par défaut pour exercises
:
@dataclass
class WorkoutSession:
exercises: List[Exercise] = []
duration_minutes: int = None
hiit_monday = WorkoutSession("25-02-2024")
ValueError: mutable default <class 'list'> for field exercises is not allowed: use default_factory
Cependant, nous avons obtenu une erreur - il s'avère que les classes de données n'autorisent pas les valeurs par défaut mutables.
Heureusement, il est possible de remédier à ce problème en utilisant une usine par défaut :
@dataclass
class WorkoutSession:
exercises: List[Exercise] = field(default_factory=list) # PAY ATTENTION
duration_minutes: int = 0
hiit_monday = WorkoutSession()
hiit_monday
WorkoutSession(exercises=[], duration_minutes=0)
Le paramètre default_factory
accepte une fonction qui renvoie une valeur initiale pour un champ de la classe de données. Cela signifie qu'il peut accepter n'importe quelle fonction arbitraire :
tuple
dict
set
- Toute fonction personnalisée définie par l'utilisateur
Ceci est exact, que le résultat de la fonction soit mutable ou non.
Si l'on y réfléchit bien, la plupart des gens commencent leur entraînement par des exercices d'échauffement qui sont généralement similaires pour tout type de séance d'entraînement. Par conséquent, l'initialisation des sessions sans exercices peut ne pas correspondre à ce que certaines personnes souhaitent.
Au lieu de cela, créons une fonction qui renvoie trois sites d'échauffement Exercises
:
def create_warmup():
return [
Exercise("Jumping jacks", 30, 1),
Exercise("Squat lunges", 10, 2),
Exercise("High jumps", 20, 1),
]
@dataclass
class WorkoutSession:
exercises: List[Exercise] = field(default_factory=create_warmup)
duration_minutes: int = 5 # Increase the default duration as well
hiit_monday = WorkoutSession()
hiit_monday
WorkoutSession(exercises=[Exercise(name='Jumping jacks', reps=30, sets=1, weight=0), Exercise(name='Squat lunges', reps=10, sets=2, weight=0), Exercise(name='High jumps', reps=20, sets=1, weight=0)], duration_minutes=5)
Désormais, chaque fois que nous créons une session, les participants sont accompagnés d'exercices d'échauffement déjà enregistrés. La nouvelle version de WorkoutSession
a une durée par défaut de cinq minutes pour en tenir compte.
Ajouter des méthodes aux classes de données
Les classes de données étant des classes normales, l'ajout de méthodes reste le même. Ajoutons deux méthodes à notre classe de données WorkoutSession
:
@dataclass
class WorkoutSession:
exercises: List[Exercise] = field(default_factory=create_warmup)
duration_minutes: int = 5
def add_exercise(self, exercise: Exercise):
self.exercises.append(exercise)
def increase_duration(self, minutes: int):
self.duration_minutes += minutes
Grâce à ces méthodes, nous pouvons désormais enregistrer toute nouvelle activité dans une session :
hiit_monday = WorkoutSession()
# Log a new exercise
new_exercise = Exercise("Deadlifts", 6, 4, 60)
hiit_monday.add_exercise(new_exercise)
hiit_monday.increase_duration(15)
Mais il y a un problème :
hiit_monday
WorkoutSession(exercises=[Exercise(name='Jumping jacks', reps=30, sets=1, weight=0), Exercise(name='Squat lunges', reps=10, sets=2, weight=0), Exercise(name='High jumps', reps=20, sets=1, weight=0), Exercise(name='Deadlifts', reps=6, sets=4, weight=60)], duration_minutes=20)
Lorsque nous imprimons la session, sa représentation par défaut est trop verbeuse et illisible puisqu'elle contient le code pour recréer l'objet. Corrigeons cela.
__repr__
et __str__
dans les classes de données
Les classes de données implémentent automatiquement __repr__
mais pas __str__
. Cela permet à la classe de se rabattre sur __repr__
lorsque nous faisons appel à print.
Remplaçons donc ce comportement en définissant nous-mêmes __str__
:
@dataclass
class Exercise:
name: str = "Push-ups"
reps: int = 10
sets: int = 3
weight: float = 0
def __str__(self):
base = f"{self.name}: {self.reps}/{self.sets}"
if self.weight == 0:
return base
return base + f", {self.weight} lbs"
ex1 = Exercise(name="Burpees", reps=15, sets=3)
ex1
Exercise(name='Burpees', reps=15, sets=3, weight=0)
Le site __repr__
est toujours le même, mais lorsque nous appelons print
sur ce site :
print(ex1)
Burpees: 15/3
La représentation printanière de la classe est beaucoup plus agréable. Maintenant, corrigeons également WorkoutSession
:
@dataclass
class WorkoutSession:
exercises: List[Exercise] = field(default_factory=create_warmup)
duration_minutes: int = 5 # Increase the default duration as well
def add_exercise(self, exercise: Exercise):
self.exercises.append(exercise)
def increase_duration(self, minutes: int):
self.duration_minutes += minutes
def __str__(self):
base = ""
for ex in self.exercises:
base += str(ex) + "\n"
base += f"\nSession duration: {self.duration_minutes} minutes."
return base
hiit_monday = WorkoutSession()
print(hiit_monday)
Jumping jacks: 30/1
Squat lunges: 10/2
High jumps: 20/1
Session duration: 5 minutes.
Note: Utilisez le bouton "Expliquer le code" au bas de l'extrait pour obtenir une explication ligne par ligne du code.
Nous avons maintenant un résultat lisible et compact.
Comparaison des classes de données
Pour de nombreuses classes, il est judicieux de comparer leurs objets selon une certaine logique. Pour les séances d'entraînement, il peut s'agir de la durée de la séance, de l'intensité de l'exercice ou du poids.
Tout d'abord, voyons ce qui se passe si nous essayons de comparer deux séances d'entraînement dans l'état actuel :
hiit_wednesday = WorkoutSession()
hiit_wednesday.add_exercise(Exercise("Pull-ups", 7, 3))
print(hiit_wednesday)
Jumping jacks: 30/1
Squat lunges: 10/2
High jumps: 20/1
Pull-ups: 7/3
Session duration: 5 minutes.
hiit_monday > hiit_wednesday
TypeError: '>' not supported between instances of 'WorkoutSession' and 'WorkoutSession'
Nous recevons TypeError
car les classes de données n'implémentent pas d'opérateurs de comparaison. Mais cela peut être facilement corrigé en réglant le paramètre order
sur True
:
@dataclass(order=True)
class WorkoutSession:
exercises: List[Exercise] = field(default_factory=create_warmup)
duration_minutes: int = 5
...
hiit_monday = WorkoutSession()
# hiit_monday.add_exercise(...)
hiit_monday.increase_duration(10)
hiit_wednesday = WorkoutSession()
hiit_monday > hiit_wednesday
True
Cette fois-ci, la comparaison fonctionne, mais qu'est-ce qu'on compare ?
Dans les classes de données, la comparaison est effectuée dans l'ordre dans lequel les champs sont définis. Actuellement, les classes sont comparées sur la base de la durée de l'entraînement car le premier champ, exercises
, contient des objets non standard.
Nous pouvons le vérifier en augmentant la durée de la session de mercredi :
hiit_monday = WorkoutSession()
# hiit_monday.add_exercise(...)
hiit_wednesday = WorkoutSession()
hiit_wednesday.increase_duration(10)
hiit_monday > hiit_wednesday
False
Comme prévu, nous avons reçu False
.
Mais que se passerait-il si le premier champ de Workout
était un autre type de champ, par exemple une chaîne de caractères ? Essayons de le savoir :
@dataclass(order=True)
class WorkoutSession:
date: str = None # DD-MM-YYYY
exercises: List[Exercise] = field(default_factory=create_warmup)
duration_minutes: int = 5
...
hiit_monday = WorkoutSession("25-02-2024")
hiit_monday.increase_duration(10)
hiit_wednesday = WorkoutSession("27-02-2024")
hiit_monday > hiit_wednesday
False
Même si la session du lundi dure plus longtemps, la comparaison montre qu'elle est plus petite que celle du mercredi. La raison en est que "25" vient avant "27" dans la comparaison de chaînes de Python.
Comment conserver l'ordre des champs tout en triant les sessions en fonction de la durée de la séance d'entraînement ? Cette opération est facile à réaliser grâce à la fonction field
:
@dataclass(order=True)
class WorkoutSession:
date: str = field(default=None, compare=False)
exercises: List[Exercise] = field(default_factory=create_warmup)
duration_minutes: int = 5
...
hiit_monday = WorkoutSession("25-02-2024")
hiit_monday.increase_duration(10)
hiit_wednesday = WorkoutSession("27-02-2024")
hiit_monday > hiit_wednesday
True
En définissant compare
sur False
pour un champ quelconque, nous l'excluons du tri, comme le montre le résultat ci-dessus.
Manipulation du champ post-init
À l'heure actuelle, la durée de la session est fixée par défaut à cinq minutes pour tenir compte des exercices d'échauffement. Toutefois, cela n'a de sens que si l'utilisateur commence une session par un échauffement. Que se passe-t-il s'ils commencent une session avec d'autres exercices ?
new_session = WorkoutSession([Exercise("Diamond push-ups", 10, 3)])
new_session.duration_minutes
5
Pour un seul exercice, la durée totale est de cinq minutes, ce qui est illogique. Chaque session doit déterminer dynamiquement sa durée en fonction du nombre de séries de chaque exercice. Cela signifie que nous devons rendre duration_minutes
dépendant du champ exercises
.
Mettons-le en œuvre :
@dataclass
class WorkoutSession:
exercises: List[Exercise] = field(default_factory=create_warmup)
duration_minutes: int = field(default=0, init=False)
def __post_init__(self):
set_duration = 3
for ex in self.exercises:
self.duration_minutes += ex.sets * set_duration
...
Cette fois, nous définissons duration_minutes
avec init
fixé à False
pour retarder l'initialisation du champ.
Ensuite, à l'intérieur d'une méthode spéciale __post_init__
, nous mettons à jour sa valeur en fonction du nombre total d'ensembles dans chaque Exercise
.
Maintenant, lorsque nous initialisons WorkoutSession
, le site duration_minutes
est dynamiquement augmenté de trois minutes pour chaque série de chaque exercice.
# Adding an exercise with three sets
hiit_friday = WorkoutSession([Exercise("Diamond push-ups", 10, 3)])
hiit_friday.duration_minutes
9
En général, si vous souhaitez définir un champ qui dépend d'autres champs de votre classe de données, vous pouvez utiliser la logique __post_init__
.
L'immutabilité dans les classes de données
Notre classe de données WorkoutSession
est presque prête ; il ne reste plus qu'à la protéger. À l'heure actuelle, il est assez facile d'y mettre le bazar :
hiit_friday.duration_minutes = 1000
hiit_friday.duration_minutes
1000
del hiit_friday.exercises
Nous voulons protéger tous les champs de nos classes afin qu'ils ne puissent être modifiés qu'à notre guise. Pour ce faire, le décorateur @dataclass
propose un argument pratique frozen
:
@dataclass(frozen=True)
class FrozenExercise:
name: str
reps: int
sets: int
weight: int | float = 0
ex1 = FrozenExercise("Muscle-ups", 5, 3)
Maintenant, si nous voulons modifier un champ, nous obtenons une erreur :
ex1.sets = 5
FrozenInstanceError: cannot assign to field 'sets'
La définition de frozen
en True
ajoute automatiquement les méthodes __deleteattr__
et __setattr__
pour chaque champ afin qu'ils soient protégés contre la suppression ou les mises à jour après l'initialisation. En outre, les autres ne pourront pas non plus ajouter de nouveaux champs :
ex1.new_field = 10
FrozenInstanceError: cannot assign to field 'new_field'
Cette fonctionnalité comporterait des dizaines de lignes de code si nous avions affaire à des classes traditionnelles.
Notez toutefois que nous ne pouvons pas rendre nos classes réellement immuables. Par exemple, réécrivons le site WorkoutSession
en remplaçant frozen
par True
:
@dataclass(frozen=True)
class ImmutableWorkoutSession:
exercises: List[Exercise] = field(default_factory=create_warmup)
duration_minutes: int = 5
session1 = ImmutableWorkoutSession()
Comme prévu, nous ne pouvons pas modifier directement la liste des exercices :
session1.exercises = [Exercise()]
Cependant, exercises
est une liste, qui est entièrement mutable, ce qui rend l'opération suivante possible :
# Changement d'un des éléments d'une liste
# Changing one of the elements in a list
session1.exercises[1] = FrozenExercise("Totally new exercise", 5, 5)
print(session1)
ImmutableWorkoutSession(exercises=[Exercise(name='Jumping jacks', reps=30, sets=1, weight=0), FrozenExercise(name='Totally new exercise', reps=5, sets=5, weight=0), Exercise(name='High jumps', reps=20, sets=1, weight=0)], duration_minutes=5)
Pour se prémunir contre les modifications accidentelles, il est donc recommandé d'utiliser des objets immuables, tels que des tuples, pour les valeurs des champs.
Héritage dans les classes de données
Un dernier point que nous aborderons est l'ordre des champs dans les classes parentales et enfantines.
Comme les classes de données sont des classes normales, l'héritage fonctionne comme d'habitude :
@dataclass(frozen=True)
class ImmutableWorkoutSession:
exercises: List[Exercise] = field(default_factory=create_warmup)
duration_minutes: int = 5
@dataclass(frozen=True)
class CardioWorkoutSession(ImmutableWorkoutSession):
pass
Mais comme le dernier champ de la classe mère (ImmutableWorkoutSession
) a une valeur par défaut, tous les champs des classes enfants doivent avoir des valeurs par défaut.
Par exemple, ceci n'est pas autorisé :
@dataclass(frozen=True)
class ImmutableWorkoutSession:
exercises: List[Exercise] = field(default_factory=create_warmup)
duration_minutes: int = 5
@dataclass(frozen=True)
class CardioWorkoutSession(ImmutableWorkoutSession):
intensity_level: str # Not allowed, must have a default
TypeError: non-default argument 'intensity_level' follows default argument
Inconvénients des classes de données et autres ressources
Les classes de données ont été constamment améliorées depuis Python 3.7 (elles étaient excellentes au départ) et couvrent de nombreux cas d'utilisation où vous pourriez avoir besoin d'écrire des classes. Mais ils peuvent être désavantageux dans les scénarios suivants :
- Méthodes personnalisées
__init__
- Méthodes personnalisées
__new__
- Différents modèles d'héritage
Et bien d'autres encore, comme en témoigne cet excellent fil de discussion sur Reddit. Si vous souhaitez une explication plus détaillée des raisons pour lesquelles les classes de données ont été introduites et pourquoi elles ne remplacent pas directement les définitions de classes ordinaires, lisez le document PEP 557.
Si vous êtes intéressé par la programmation orientée objet en général, voici un cours qui vous permettra de poursuivre votre voyage :
Fondamentalement, les classes de données sont des structures plus sophistiquées qui permettent de conserver et d'extraire des données plus efficacement. Cependant, Python dispose de nombreuses autres structures de données qui effectuent cette tâche de manière plus ou moins similaire. Par exemple, vous pouvez en apprendre davantage sur les compteurs, defaultdicts
et namedtuples
dans le dernier chapitre du cours Data Types for Data Science.

Je suis un créateur de contenu en science des données avec plus de 2 ans d'expérience et l'un des plus grands followings sur Medium. J'aime écrire des articles détaillés sur l'IA et la ML dans un style un peu sarcastıc, car il faut bien faire quelque chose pour les rendre un peu moins ennuyeux. J'ai produit plus de 130 articles et un cours DataCamp, et un autre est en cours d'élaboration. Mon contenu a été vu par plus de 5 millions de personnes, dont 20 000 sont devenues des adeptes sur Medium et LinkedIn.
Continuez à apprendre Python
cursus
Python Data Fundamentals
cours
Introduction to Functions in Python
blog
2022-2023 Rapport annuel DataCamp Classrooms
blog
Q2 2023 DataCamp Donates Digest
blog
Nous avons fait don de bourses DataCamp Premium à un million de personnes, et ce n'est pas fini.
blog
Les 32 meilleures questions d'entretien sur AWS et leurs réponses pour 2024
blog
Célébration de Saghar Hazinyar : Une boursière de DataCamp Donates et une diplômée de Code to Inspire

Fereshteh Forough
4 min
blog
Les 20 meilleures questions d'entretien pour les flocons de neige, à tous les niveaux

Nisha Arya Ahmed
20 min