Aller au contenu
Réussir mon BTS SIO

Tape une notion, une commande ou le nom d'une épreuve.

Par exemple

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

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])
text
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 écrit Serveur("srv-ad", …), juste après avoir créé l'objet. Il range les valeurs reçues dans les attributs de l'objet.
  • self est 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() devient Serveur.est_linux(ad).
  • __str__ fournit le texte lisible affiché par print() ; __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, publicPas 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 : Machineclass Serveur(Machine):
InterfacePas 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.

python
# 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
text
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 :

python
# 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))
text
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 :

python
# 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))
text
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 toiEffet 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=TrueUn 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
python
# 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)
text
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
python
# 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}")
text
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
python
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 :

text
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é ?

Chapitre terminé !

Fais les 3 exercices avant de passer à la suite : c'est en t'entraînant que tout se fixe.

chapitres
–
de série
–
du cours
–
Chapitre suivant : Tester son code Python avec pytestRevoir le sommaire du cours