Chapitre 1418 minDébutant
Classes et dataclasses en Python (POO)
Par Josselyn C., prof de BTS SIO. Mis à jour le .
À la fin de ce chapitre, tu sauras
- écrire une classe avec ses attributs et ses méthodes ;
- valider un attribut avec une propriété ;
- réutiliser du code par héritage ;
- décrire des données avec une dataclass et une énumération.
3 exercices corrigés
Une classe est un modèle qui regroupe des données (les attributs) et les opérations sur ces données (les méthodes) ; chaque objet créé à partir d'elle en est une instance. Ce chapitre montre la programmation orientée objet à la façon de Python : __init__ et self, les méthodes spéciales __str__ et __repr__, les propriétés, l'héritage, puis les dataclasses, qui écrivent pour toi le code répétitif d'une classe de données, et les énumérations. La théorie de la POO (encapsulation, associations, polymorphisme, interfaces) est développée dans le cours Programmation orientée objet, en C#.
Classe et objet en Python
# classe.py : une classe Serveur, ses attributs et ses méthodes
class Serveur:
"""Un serveur du parc."""
def __init__(self, nom, ip, systeme):
self.nom = nom # attributs propres à chaque objet
self.ip = ip
self.systeme = systeme
self.en_service = True
def est_linux(self):
return self.systeme.startswith(("Debian", "Ubuntu"))
def mettre_hors_service(self):
self.en_service = False
def __str__(self): # ce qu'affiche print()
etat = "en service" if self.en_service else "hors service"
return f"{self.nom} ({self.ip}, {self.systeme}) : {etat}"
def __repr__(self): # ce qu'affiche la console, et une liste d'objets
return f"Serveur({self.nom!r}, {self.ip!r}, {self.systeme!r})"
ad = Serveur("srv-ad", "192.168.10.177", "Windows Server 2025")
journaux = Serveur("srv-journaux", "192.168.10.180", "Debian 13")
journaux.mettre_hors_service()
print(ad)
print(journaux)
print(ad.est_linux(), journaux.est_linux())
print([ad, journaux])
srv-ad (192.168.10.177, Windows Server 2025) : en service
srv-journaux (192.168.10.180, Debian 13) : hors service
False True
[Serveur('srv-ad', '192.168.10.177', 'Windows Server 2025'), Serveur('srv-journaux', '192.168.10.180', 'Debian 13')]
class Serveur:déclare la classe. Par convention, son nom s'écrit en CamelCase.__init__est l'initialiseur : Python l'appelle quand on écritServeur("srv-ad", …), juste après avoir créé l'objet. Il range les valeurs reçues dans les attributs de l'objet.selfest l'objet lui-même. En Python, il est explicite : premier paramètre de chaque méthode, et préfixe de chaque attribut (self.nom). À l'appel, on ne le passe pas :ad.est_linux()devientServeur.est_linux(ad).__str__fournit le texte lisible affiché parprint();__repr__, le texte « technique » affiché par la console et dans une liste. Une bonne pratique :__repr__ressemble à l'appel qui recrée l'objet. Les noms entourés de doubles tirets bas sont les méthodes spéciales, que Python appelle lui-même.
startswith accepte un tuple de débuts possibles : ("Debian", "Ubuntu").
| C# (cours POO) | Python |
|---|---|
Constructeur public Serveur(...) | def __init__(self, ...) |
this.nom (souvent implicite) | self.nom (toujours écrit) |
private, protected, public | Pas de mot-clé : un nom qui commence par _ signifie « interne, n'y touche pas » |
ToString() | __str__ et __repr__ |
Propriété get / set | @property et @nom.setter |
class Serveur : Machine | class Serveur(Machine): |
| Interface | Pas de mot-clé : on écrit le code qui appelle les méthodes attendues (duck typing), ou une classe abstraite du module abc |
Les propriétés
Une propriété s'utilise comme un attribut, mais exécute du code : pour calculer une valeur, ou pour valider une affectation, ce qui protège l'objet d'un état absurde.
# propriete.py : un attribut calculé et un attribut protégé par une validation
class Volume:
def __init__(self, nom, capacite_go, utilise_go):
self.nom = nom
self.capacite_go = capacite_go
self.utilise_go = utilise_go # passe par le setter ci-dessous
@property
def taux(self):
"""Taux d'occupation en pourcentage, calculé à chaque lecture."""
return self.utilise_go / self.capacite_go * 100
@property
def utilise_go(self):
return self._utilise_go
@utilise_go.setter
def utilise_go(self, valeur):
if not 0 <= valeur <= self.capacite_go:
raise ValueError(f"{self.nom} : {valeur} Go utilisés pour {self.capacite_go} Go")
self._utilise_go = valeur
partages = Volume("partages", 500, 437.5)
print(f"{partages.taux:.1f} %")
partages.utilise_go = 450
print(f"{partages.taux:.1f} %")
partages.utilise_go = 520
87.5 %
90.0 %
Traceback (most recent call last):
File "/home/etudiant/cours-python/propriete.py", line 28, in <module>
partages.utilise_go = 520
^^^^^^^^^^^^^^^^^^^
File "/home/etudiant/cours-python/propriete.py", line 20, in utilise_go
raise ValueError(f"{self.nom} : {valeur} Go utilisés pour {self.capacite_go} Go")
ValueError: partages : 520 Go utilisés pour 500 Go
taux n'est stocké nulle part : il est recalculé à chaque lecture, donc toujours juste. utilise_go est stocké dans _utilise_go (le tiret bas dit « interne ») et chaque affectation, y compris celle de __init__, passe par le setter, qui refuse 520 Go sur un volume de 500. C'est l'encapsulation (encapsulation en C#).
L'héritage
Une classe fille hérite des attributs et des méthodes de sa classe mère, et peut en ajouter ou en redéfinir :
# heritage.py : une classe mère Machine et deux classes filles
class Machine:
def __init__(self, nom, ip):
self.nom = nom
self.ip = ip
def description(self):
return f"{self.nom} ({self.ip})"
class Serveur(Machine):
def __init__(self, nom, ip, role):
super().__init__(nom, ip) # la classe mère initialise nom et ip
self.role = role
def description(self): # redéfinition
return f"Serveur {super().description()} : {self.role}"
class Imprimante(Machine):
def __init__(self, nom, ip, toner):
super().__init__(nom, ip)
self.toner = toner
def description(self):
alerte = " - toner à commander" if self.toner < 15 else ""
return f"Imprimante {super().description()}, toner {self.toner} %{alerte}"
parc = [Serveur("srv-nas", "192.168.10.183", "stockage TrueNAS"),
Imprimante("IMP-COMPTA", "192.168.10.88", 12),
Machine("PC-0042", "192.168.10.92")]
for machine in parc:
print(machine.description()) # chaque objet utilise SA méthode
print(isinstance(parc[0], Machine), isinstance(parc[2], Serveur))
Serveur srv-nas (192.168.10.183) : stockage TrueNAS
Imprimante IMP-COMPTA (192.168.10.88), toner 12 % - toner à commander
PC-0042 (192.168.10.92)
True False
super().__init__(nom, ip) appelle l'initialiseur de la classe mère, qui s'occupe de ce qu'elle connaît ; super().description() réutilise sa méthode. La boucle appelle description() sur trois objets de classes différentes et chacun répond à sa façon : c'est le polymorphisme. isinstance(objet, Classe) teste si un objet est une instance d'une classe ou de ses filles : un Serveur est une Machine, l'inverse n'est pas vrai (héritage en C#).
Les dataclasses
La plupart des classes d'un script d'administration ne font que porter des données : un poste, un compte, un ticket. Écrire leur __init__, leur __repr__ et leur comparaison à la main est répétitif ; le décorateur @dataclass les génère à partir des annotations de type :
# donnees.py : des dataclasses pour l'inventaire
import json
from dataclasses import asdict, dataclass, field
from datetime import date
from enum import StrEnum
class Etat(StrEnum):
EN_SERVICE = "en service"
EN_STOCK = "en stock"
REBUT = "rebut"
@dataclass
class Poste:
nom: str
service: str
fin_garantie: date
etat: Etat = Etat.EN_SERVICE
logiciels: list[str] = field(default_factory=list) # une nouvelle liste par objet
def sous_garantie(self, jour: date) -> bool:
return jour <= self.fin_garantie
p1 = Poste("PC-0012", "Comptabilité", date(2025, 3, 15), logiciels=["Sage 100"])
p2 = Poste("PC-0050", "Logistique", date(2028, 2, 17))
print(p1)
print(p1.sous_garantie(date(2026, 11, 8)), p2.sous_garantie(date(2026, 11, 8)))
print(p2 == Poste("PC-0050", "Logistique", date(2028, 2, 17)))
p2.etat = Etat("en stock") # retrouver un membre de l'énumération par sa valeur
print(repr(p2.etat), p2.etat)
dico = asdict(p1)
print(json.dumps(dico, default=str, ensure_ascii=False))
Poste(nom='PC-0012', service='Comptabilité', fin_garantie=datetime.date(2025, 3, 15), etat=<Etat.EN_SERVICE: 'en service'>, logiciels=['Sage 100'])
False True
True
<Etat.EN_STOCK: 'en stock'> en stock
{"nom": "PC-0012", "service": "Comptabilité", "fin_garantie": "2025-03-15", "etat": "en service", "logiciels": ["Sage 100"]}
Ce que @dataclass écrit pour toi | Effet ici |
|---|---|
__init__ | Poste("PC-0012", "Comptabilité", date(2025, 3, 15)), avec les valeurs par défaut |
__repr__ | L'affichage complet de l'objet |
__eq__ | Deux postes aux attributs égaux sont égaux (== vaut True) |
Avec order=True | <, > et donc sorted(), en comparant les attributs dans l'ordre |
Avec frozen=True | Un objet non modifiable, utilisable comme clé de dictionnaire |
Le champ logiciels utilise field(default_factory=list) : comme pour une fonction (la valeur par défaut modifiable), une liste par défaut écrite = [] serait partagée par tous les postes ; Python refuse d'ailleurs de l'écrire ainsi dans une dataclass. asdict() convertit l'objet en dictionnaire, prêt pour json.dumps ; default=str y convertit la date en texte.
Une énumération (Enum) définit un ensemble fermé de valeurs nommées : l'état d'un poste ne peut être que EN_SERVICE, EN_STOCK ou REBUT, et une faute de frappe (Etat.EN_SERIVCE) provoque une erreur au lieu de passer inaperçue. StrEnum (Python 3.11 et plus) en fait aussi des chaînes : elles s'affichent et s'enregistrent en JSON par leur valeur. Etat("en stock") retrouve le membre à partir de sa valeur, par exemple lue dans un CSV.
Exercices
Exercice 1 sur 3 Un compte qui se verrouille Réussi À revoir
Écris une classe Compte avec un login, un service, un état actif et un compteur d'échecs de connexion. La méthode echec_connexion(seuil=5) incrémente le compteur et désactive le compte quand il atteint le seuil ; __str__ affiche login (service) : actif, n échec(s) ou DÉSACTIVÉ. Simule 4 échecs, affiche le compte, puis un cinquième.
Voir la réponseMasquer la réponse de l'exercice 1
# ex_compte.py : une classe Compte avec désactivation
class Compte:
def __init__(self, login, service):
self.login = login
self.service = service
self.actif = True
self.echecs = 0
def echec_connexion(self, seuil=5):
self.echecs += 1
if self.echecs >= seuil:
self.desactiver()
def desactiver(self):
self.actif = False
def __str__(self):
etat = "actif" if self.actif else "DÉSACTIVÉ"
return f"{self.login} ({self.service}) : {etat}, {self.echecs} échec(s)"
compte = Compte("tmarchal", "Logistique")
for _ in range(4):
compte.echec_connexion()
print(compte)
compte.echec_connexion()
print(compte)
tmarchal (Logistique) : actif, 4 échec(s)
tmarchal (Logistique) : DÉSACTIVÉ, 5 échec(s)
for _ in range(4) : le tiret bas est le nom conventionnel d'une variable dont on ne se sert pas. La méthode echec_connexion appelle self.desactiver() : une méthode peut en appeler une autre du même objet. C'est la logique du verrouillage de compte d'Active Directory, configurée par une stratégie de mot de passe (exemples de GPO).
Tu avais trouvé ?
Exercice 2 sur 3 Trier les tickets Réussi À revoir
Crée une dataclass Ticket (priorité de 1, critique, à 4, basse ; date d'ouverture ; numéro ; titre) qui se trie d'abord par priorité, puis du plus ancien au plus récent. Trie ces quatre tickets et affiche-les : #1201 (priorité 3, ouvert le 6 novembre 2026 à 9 h 05, « Imprimante de la compta hors ligne »), #1214 (priorité 1, le 8 à 7 h 40, « Plus d'accès au partage Logistique »), #1209 (priorité 2, le 7 à 16 h 12, « Lenteurs de l'outil de gestion »), #1212 (priorité 1, le 8 à 6 h 55, « Scanners de l'entrepôt sans réseau »).
Voir la réponseMasquer la réponse de l'exercice 2
# ex_tickets.py : trier des tickets par priorité, puis par ancienneté
from dataclasses import dataclass
from datetime import datetime
@dataclass(order=True)
class Ticket:
priorite: int # 1 = critique ... 4 = basse : comparé en premier
ouvert_le: datetime # puis la date : le plus ancien d'abord
numero: int
titre: str
tickets = [
Ticket(3, datetime(2026, 11, 6, 9, 5), 1201, "Imprimante de la compta hors ligne"),
Ticket(1, datetime(2026, 11, 8, 7, 40), 1214, "Plus d'accès au partage Logistique"),
Ticket(2, datetime(2026, 11, 7, 16, 12), 1209, "Lenteurs de l'outil de gestion"),
Ticket(1, datetime(2026, 11, 8, 6, 55), 1212, "Scanners de l'entrepôt sans réseau"),
]
for t in sorted(tickets):
print(f"P{t.priorite} #{t.numero} {t.ouvert_le:%d/%m %H:%M} {t.titre}")
P1 #1212 08/11 06:55 Scanners de l'entrepôt sans réseau
P1 #1214 08/11 07:40 Plus d'accès au partage Logistique
P2 #1209 07/11 16:12 Lenteurs de l'outil de gestion
P3 #1201 06/11 09:05 Imprimante de la compta hors ligne
Avec order=True, deux tickets se comparent attribut par attribut, dans l'ordre de déclaration : d'abord la priorité, puis la date en cas d'égalité. L'ordre des champs est donc un choix de conception. Les deux tickets de priorité 1 sortent du plus ancien (6 h 55) au plus récent (7 h 40). Ce tri reprend la logique de priorisation d'ITIL (cours ITIL).
Tu avais trouvé ?
Exercice 3 sur 3 Polymorphisme Réussi À revoir
Ajoute à l'exemple de l'héritage une classe Poste(Machine) qui reçoit en plus le nom de l'utilisateur, et dont description() renvoie par exemple Poste PC-0071 (192.168.10.104), utilisé par Inès Roux. Que faut-il modifier dans la boucle qui affiche le parc ?
Voir la réponseMasquer la réponse de l'exercice 3
class Poste(Machine):
def __init__(self, nom, ip, utilisateur):
super().__init__(nom, ip)
self.utilisateur = utilisateur
def description(self):
return f"Poste {super().description()}, utilisé par {self.utilisateur}"
parc = [Machine("IMP-ACCUEIL", "192.168.10.89"), Poste("PC-0071", "192.168.10.104", "Inès Roux")]
for machine in parc:
print(machine.description())
Placé sous la classe Machine de l'exemple de l'héritage, le programme affiche :
IMP-ACCUEIL (192.168.10.89)
Poste PC-0071 (192.168.10.104), utilisé par Inès Roux
Rien à modifier dans la boucle : elle appelle description() sur chaque objet, et le Poste répond avec sa propre méthode. C'est tout l'intérêt du polymorphisme : ajouter un type de machine ne touche pas au code qui les parcourt.
Tu avais trouvé ?