Objet, classe, interface
Pourquoi des objets ?
Les structures de données du chapitre — files, piles, arbres, graphes — ont une particularité commune : elles regroupent un état (les éléments stockés) et un comportement (les opérations qu'on peut faire dessus). Tant qu'on ne manipule qu'une seule pile ou une seule file dans un programme jouet, on peut s'en sortir avec une liste Python et quelques fonctions libres. Dès qu'on en a plusieurs, ou qu'on veut protéger la cohérence interne, cette approche craque : rien n'empêche un appelant distrait de modifier la liste interne et de casser l'invariant.
{"solde": 100}, n'importe qui peut écrire compte["solde"] = -1000 sans
qu'aucune règle métier n'intervienne. Le compte n'a plus aucun garde-fou.Le paradigme objet offre un cadre linguistique parfaitement adapté à cette situation. Une classe réunit en un même bloc le plan des données internes (les attributs) et le catalogue des opérations disponibles (les méthodes). Un objet est une instance concrète de ce plan — une analogie utile : la classe est le plan d'architecte, l'objet est la maison construite. Plusieurs maisons peuvent être bâties depuis un même plan, chacune avec ses meubles à elle.
Cet item ne demande ni l'héritage ni le polymorphisme. L'objectif est plus modeste : savoir écrire une définition de classe Python, l'instancier, accéder à ses attributs et appeler ses méthodes.
Classe et instance
Reprenons le compte bancaire. Une définition minimale en Python :
class CompteBancaire:
"""Compte bancaire avec un solde et des opérations de dépôt/retrait."""
def __init__(self, solde_initial=0):
self.solde = solde_initial # attribut d'instance
Trois éléments à comprendre :
- Le mot-clé
classintroduit la définition, suivi du nom de la classe en PascalCase (CompteBancaire). __init__est une méthode spéciale appelée à la création d'un objet — on l'appelle constructeur. Son rôle : initialiser les attributs.selfest la convention Python pour désigner l'objet courant au sein d'une méthode. C'est obligatoirement le premier paramètre.
L'instanciation consiste à appeler la classe comme une fonction :
c = CompteBancaire(100) # appel implicite de __init__(self, solde_initial=100)
print(c.solde) # 100
Chaque appel CompteBancaire(...) crée un objet indépendant : ses attributs
lui sont propres.
c1 = CompteBancaire(100)
c2 = CompteBancaire(50)
c1.solde = 200
print(c1.solde, c2.solde) # 200 50 — c2 n'a pas bougé
Dans c = CompteBancaire(100), à quel paramètre du init la valeur 100 est-elle passée ?
Méthodes — donner un comportement aux objets
Une méthode est une fonction définie dans la classe ; elle agit sur les
attributs de l'objet via self. Ajoutons à CompteBancaire les opérations
attendues d'un vrai compte :
class CompteBancaire:
def __init__(self, solde_initial=0):
self.solde = solde_initial
def deposer(self, montant):
self.solde = self.solde + montant
def retirer(self, montant):
self.solde = self.solde - montant
def consulter(self):
return self.solde
Une méthode s'appelle sur un objet avec la notation pointée :
c = CompteBancaire(100)
c.deposer(50)
c.retirer(20)
print(c.consulter()) # 130
Quand vous écrivez c.deposer(50), Python traduit en
CompteBancaire.deposer(c, 50) — l'objet c est passé en premier argument, qui
porte le nom self par convention. C'est tout le mystère de self.
Dans c.deposer(50), à quoi correspond le paramètre self ?
carre(5)). Une méthode est définie dans une classe ; on l'appelle sur un
objet (c.deposer(50)) et elle a accès aux attributs de cet objet via self.L'interface d'une classe
Une classe expose vers l'extérieur un ensemble d'opérations — son
interface — et cache vers l'intérieur les détails de réalisation — son
implémentation. Pour CompteBancaire, l'interface naturelle est :
| Méthode | Effet attendu |
|---|---|
deposer(montant) | Augmente le solde de montant. |
retirer(montant) | Diminue le solde de montant, à condition qu'il reste positif. |
consulter() | Renvoie le solde courant. |
Cette spécification — le contrat que la classe promet de respecter — est
indépendante de la façon dont elle est codée. C'est elle que voit l'utilisateur
de la classe ; lui n'a pas à savoir si le solde est stocké dans un entier, dans
un flottant, ou dans une base de données distante. Tant que consulter()
renvoie le bon nombre, le contrat est tenu.
Une analogie utile : l'interface est un contrat de sous-traitance. Le donneur d'ordre liste ce qu'il veut recevoir (les opérations et leur comportement) ; peu lui importe quelle usine fabrique, ni avec quelle machine. Tant que le contrat est respecté, on peut changer d'usine sans rien renégocier en amont.
Cette séparation est au cœur de . Elle vous permet de raisonner sur un programme sans dépendre du code interne d'une classe, et d'échanger une implémentation par une autre sans rien casser ailleurs.
Encapsulation — protéger les invariants
Un invariant est une propriété que les attributs d'un objet doivent
toujours vérifier. Pour notre compte : « le solde reste positif ou nul ». La
version actuelle de retirer ne protège rien :
c = CompteBancaire(50)
c.retirer(200)
print(c.consulter()) # -150 — invariant brisé
Le rôle de l'encapsulation est de placer le contrôle dans la méthode, à l'unique endroit où la modification se fait :
class CompteBancaire:
def __init__(self, solde_initial=0):
self._solde = solde_initial # attribut "interne", préfixe _ par convention
def deposer(self, montant):
if montant <= 0:
raise ValueError("le montant doit être strictement positif")
self._solde = self._solde + montant
def retirer(self, montant):
if montant <= 0:
raise ValueError("le montant doit être strictement positif")
if montant > self._solde:
raise ValueError("solde insuffisant")
self._solde = self._solde - montant
def consulter(self):
return self._solde
Deux changements méritent un commentaire :
- L'attribut s'appelle désormais
_soldeavec un préfixe_. Cette convention signale au lecteur que l'attribut est interne à la classe et qu'on ne doit pas le toucher de l'extérieur. Python n'impose aucune protection technique — vous pouvez écrirec._solde = -1000et cela marchera. Le préfixe est un engagement social entre le rédacteur de la classe et son utilisateur. - L'accès à la valeur passe par la méthode
consulter()plutôt que par l'attribut. Si demain on stocke le solde différemment (en centimes au lieu d'euros, par exemple), l'utilisateur ne s'en rendra pas compte — seule la méthode change.
_ n'est pas une protection technique. Python n'empêche pas
l'accès. C'est une convention. Si vous lisez c._solde depuis l'extérieur,
vous brisez l'abstraction même si Python vous laisse faire — et vous perdez
la garantie que les invariants tiennent.Que se passe-t-il à l'exécution de c.retirer(200) sur un compte au solde 50, dans la version encapsulée ?
Conventions et notations
- Nom de classe : PascalCase (
CompteBancaire,Pile,Noeud). - Attribut interne : préfixe
_indiquant « privé par convention » (self._solde). - Méthode magique : encadrée par
__(__init__,__len__,__repr__). Vous verrez__init__partout dans le chapitre ; les autres sont optionnelles. - Une méthode = un verbe :
deposer,retirer,consulter,enfiler,defiler. L'attribut est un nom (solde,valeur).
Pour aller plus loin
La méthode spéciale __repr__ permet de définir comment un objet s'affiche
quand on le print :
class CompteBancaire:
# ... reste de la classe inchangé ...
def __repr__(self):
return f"CompteBancaire(solde={self._solde})"
c = CompteBancaire(150)
print(c) # CompteBancaire(solde=150)
Vous reverrez les classes dans tout le reste du chapitre : FileTableau et
FileDeuxPiles au cours suivant pour illustrer la pluralité des
implémentations ; Pile (LIFO) et File (FIFO) au cours 3 pour les structures
linéaires canoniques ; Noeud au cours 4 pour les arbres ; GrapheMatrice et
GrapheListe au cours 5 pour les graphes. Le vocabulaire posé ici sera utilisé
partout.