La programmation orientée objet
Cours complet · NSI (terminale), chapitre 9 · terminale, spécialité numérique et sciences informatiques
Travailler ce chapitre sur Adloun Exercices corrigés de ce chapitre
La programmation orientée objet (POO) est un paradigme de programmation, c'est-à-dire une manière de concevoir et d'organiser un programme. Plutôt que de séparer d'un côté les données et de l'autre les fonctions qui les manipulent, la POO regroupe ces deux aspects au sein d'entités cohérentes appelées objets. Chaque objet possède des caractéristiques (les attributs) et des comportements (les méthodes).
Ce paradigme, omniprésent dans le développement logiciel moderne (Python, Java, C++, etc.), facilite la modélisation de problèmes complexes, favorise la réutilisation du code et améliore sa maintenabilité. Dans ce chapitre, nous étudierons les notions fondamentales de classes et d'objets, le mécanisme du constructeur, la référence self, le principe d'encapsulation, les méthodes spéciales, ainsi qu'une introduction à l'héritage. Nous mettrons enfin ces concepts en pratique en réimplémentant les structures de données du programme (pile, file, arbre binaire de recherche) selon une approche objet.
9.1 Classes et objets
9.1.1 Notion de classe
Une classe peut être vue comme un plan de construction ou un moule : elle décrit la structure et le comportement communs à tout un ensemble d'objets, sans être elle-même un objet concret.
Une classe est un modèle (ou type) définissant un ensemble d'attributs (les données caractérisant les objets) et de méthodes (les fonctions agissant sur ces objets). Elle sert de patron à partir duquel on peut créer des objets.
Un objet est une réalisation concrète d'une classe, dotée de ses propres valeurs d'attributs. On dit aussi que l'objet est une instance de la classe. Créer un objet à partir d'une classe s'appelle l'instanciation.
En Python, une classe se définit avec le mot-clé class. Par convention, le nom d'une classe commence par une majuscule (notation CamelCase).
class Point:
"""Un point du plan repere par ses coordonnees."""
pass
# Instanciation : creation de deux objets de la classe Point
a = Point()
b = Point()
print(type(a)) # <class '__main__.Point'>
print(a is b) # False : ce sont deux objets distincts
9.1.2 Attributs et méthodes
Un attribut est une variable associée à un objet (attribut d'instance) ou à une classe (attribut de classe). Il représente une donnée caractérisant l'objet, par exemple l'abscisse d'un point.
Une méthode est une fonction définie à l'intérieur d'une classe. Elle décrit un comportement de l'objet et peut accéder à ses attributs. Son premier paramètre est toujours self, qui désigne l'objet sur lequel la méthode est appelée.
9.2 Le constructeur et la référence self
9.2.1 La méthode __init__
Méthode : Le constructeur `__init__`
La méthode __init__ est le constructeur de la classe. Elle est appelée automatiquement lors de l'instanciation et sert à initialiser les attributs du nouvel objet. Sa signature est typiquement :
def __init__(self, parametre1, parametre2, ...):
self.attribut1 = parametre1
self.attribut2 = parametre2
Méthode : La référence `self`
self est le premier paramètre de toute méthode d'instance. Il désigne l'objet courant, celui sur lequel la méthode est appelée. Il permet d'accéder à ses attributs (self.x) et à ses autres méthodes. Python le passe automatiquement : lors de l'appel p.deplacer(1, 2), l'objet p est transmis comme self.
class Point:
def __init__(self, x, y):
self.x = x # attribut d'instance abscisse
self.y = y # attribut d'instance ordonnee
def deplacer(self, dx, dy):
"""Translate le point du vecteur (dx, dy)."""
self.x += dx
self.y += dy
def distance_origine(self):
"""Renvoie la distance du point a l'origine."""
return (self.x ** 2 + self.y ** 2) ** 0.5
p = Point(3, 4)
print(p.x, p.y) # 3 4
print(p.distance_origine()) # 5.0
p.deplacer(1, -1)
print(p.x, p.y) # 4 3
La distance à l'origine d'un point vaut .
Un attribut défini dans __init__ via self est propre à chaque objet : modifier p.x n'affecte pas les autres points. Un attribut défini directement dans le corps de la classe est un attribut de classe, partagé par toutes les instances.
class Compteur:
nb_instances = 0 # attribut de classe (partage)
def __init__(self):
Compteur.nb_instances += 1
self.id = Compteur.nb_instances # attribut d'instance
c1 = Compteur(); c2 = Compteur()
print(Compteur.nb_instances) # 2
print(c1.id, c2.id) # 1 2
9.3 Encapsulation
L'encapsulation est le principe consistant à regrouper les données et les méthodes qui les manipulent au sein d'un même objet, et à contrôler l'accès aux données depuis l'extérieur. Elle permet de protéger l'état interne d'un objet et de n'exposer qu'une interface maîtrisée.
En Python, il n'existe pas de mécanisme strict de protection comme dans d'autres langages. On utilise une convention de nommage : un attribut préfixé d'un underscore (_solde) est considéré comme « privé » et ne devrait pas être manipulé directement depuis l'extérieur de la classe.
Méthode : Accesseurs et mutateurs
Pour respecter l'encapsulation, on accède aux attributs « privés » par l'intermédiaire de méthodes dédiées :
- un accesseur (getter) renvoie la valeur d'un attribut ;
- un mutateur (setter) modifie un attribut en contrôlant la validité de la nouvelle valeur.
class CompteBancaire:
def __init__(self, titulaire, solde=0):
self.titulaire = titulaire
self._solde = solde # attribut "prive"
def get_solde(self): # accesseur
return self._solde
def deposer(self, montant):
if montant > 0:
self._solde += montant
def retirer(self, montant): # mutateur controle
if 0 < montant <= self._solde:
self._solde -= montant
return True
return False
c = CompteBancaire("Alice", 100)
c.deposer(50)
print(c.get_solde()) # 150
print(c.retirer(200)) # False : retrait refuse
print(c.get_solde()) # 150
Ici, le solde ne peut jamais devenir négatif, car toute modification passe par les méthodes deposer et retirer qui vérifient la cohérence.
9.4 Méthodes spéciales
Python définit des méthodes spéciales (ou « dunder », pour double underscore) dont le nom est encadré de deux underscores. Elles permettent d'intégrer les objets aux mécanismes du langage : affichage, comparaison, longueur, etc. Nous avons déjà rencontré __init__.
Méthode : `__str__` et `__repr__`
__str__(self)renvoie une chaîne lisible destinée à l'utilisateur ; elle est utilisée parprintetstr.__repr__(self)renvoie une représentation technique, si possible non ambiguë, destinée au développeur ; elle est utilisée dans l'interpréteur et parrepr.
Si __str__ n'est pas défini, Python utilise __repr__ comme solution de repli.
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __str__(self):
return f"({self.x}, {self.y})"
def __repr__(self):
return f"Point({self.x}, {self.y})"
p = Point(3, 4)
print(p) # (3, 4) -> __str__
print(str(p)) # (3, 4) -> __str__
print(repr(p)) # Point(3, 4) -> __repr__
print([p]) # [Point(3, 4)] -> __repr__ dans un conteneur
De nombreuses méthodes spéciales existent. Les plus utiles en NSI sont :
__len__(self): appelée parlen(objet);__eq__(self, autre): appelée par l'opérateur==;__add__(self, autre): surcharge de l'opérateur+.
class Vecteur:
def __init__(self, x, y):
self.x, self.y = x, y
def __add__(self, autre):
return Vecteur(self.x + autre.x, self.y + autre.y)
def __eq__(self, autre):
return self.x == autre.x and self.y == autre.y
def __repr__(self):
return f"Vecteur({self.x}, {self.y})"
print(Vecteur(1, 2) + Vecteur(3, 4)) # Vecteur(4, 6)
print(Vecteur(1, 2) == Vecteur(1, 2)) # True
9.5 Héritage
L'héritage est un mécanisme permettant de définir une nouvelle classe (la classe fille ou sous-classe) à partir d'une classe existante (la classe mère ou super-classe). La classe fille hérite des attributs et méthodes de la classe mère, qu'elle peut enrichir (ajout de nouvelles méthodes) ou redéfinir (surcharge).
Méthode : Héritage et fonction `super`
En Python, on indique la classe mère entre parenthèses : class Fille(Mere):. La fonction super() permet d'appeler une méthode de la classe mère, notamment son constructeur.
class Animal:
def __init__(self, nom):
self.nom = nom
def crier(self):
return "..."
def presenter(self):
return f"{self.nom} fait {self.crier()}"
class Chien(Animal):
def crier(self): # redefinition (surcharge)
return "Wouf"
class Chat(Animal):
def __init__(self, nom, race):
super().__init__(nom) # appel du constructeur parent
self.race = race
def crier(self):
return "Miaou"
print(Chien("Rex").presenter()) # Rex fait Wouf
print(Chat("Felix", "siamois").crier()) # Miaou
La méthode presenter, héritée par Chien, appelle la version redéfinie de crier : c'est le polymorphisme.
9.6 Implémentation de structures de données en POO
La POO offre un cadre naturel pour implémenter des structures de données abstraites : on définit une classe dont l'interface (les méthodes publiques) correspond aux opérations autorisées, tout en cachant la représentation interne (encapsulation).
9.6.1 La pile
Une pile (stack) est une structure de données linéaire de type LIFO (Last In, First Out) : le dernier élément ajouté est le premier retiré. Ses opérations principales sont empiler, depiler et le test de vacuité.
class Pile:
def __init__(self):
self._elements = [] # representation interne
def est_vide(self):
return len(self._elements) == 0
def empiler(self, valeur):
self._elements.append(valeur)
def depiler(self):
if self.est_vide():
raise IndexError("depiler sur une pile vide")
return self._elements.pop()
def sommet(self):
if self.est_vide():
raise IndexError("pile vide")
return self._elements[-1]
def __len__(self):
return len(self._elements)
def __str__(self):
return "Pile" + str(self._elements)
p = Pile()
p.empiler(1); p.empiler(2); p.empiler(3)
print(p) # Pile[1, 2, 3]
print(p.depiler()) # 3
print(len(p)) # 2
9.6.2 La file
Une file (queue) est une structure linéaire de type FIFO (First In, First Out) : le premier élément ajouté (enfiler) est le premier retiré (défiler).
from collections import deque
class File:
def __init__(self):
self._elements = deque() # efficace aux deux bouts
def est_vide(self):
return len(self._elements) == 0
def enfiler(self, valeur):
self._elements.append(valeur) # ajout en queue
def defiler(self):
if self.est_vide():
raise IndexError("defiler sur une file vide")
return self._elements.popleft() # retrait en tete
def __len__(self):
return len(self._elements)
f = File()
f.enfiler('a'); f.enfiler('b'); f.enfiler('c')
print(f.defiler()) # a
print(f.defiler()) # b
print(len(f)) # 1
L'emploi de deque rend le défilage en , là où une liste classique imposerait un coût en .
9.6.3 L'arbre binaire de recherche
Un arbre binaire de recherche (ABR) est un arbre binaire dans lequel, pour tout nœud de valeur , toutes les valeurs du sous-arbre gauche sont strictement inférieures à et toutes celles du sous-arbre droit lui sont supérieures. Cette propriété d'ordre permet une recherche efficace.
class Noeud:
def __init__(self, valeur):
self.valeur = valeur
self.gauche = None
self.droit = None
class ABR:
def __init__(self):
self.racine = None
def inserer(self, valeur):
self.racine = self._inserer(self.racine, valeur)
def _inserer(self, noeud, valeur):
if noeud is None:
return Noeud(valeur)
if valeur < noeud.valeur:
noeud.gauche = self._inserer(noeud.gauche, valeur)
elif valeur > noeud.valeur:
noeud.droit = self._inserer(noeud.droit, valeur)
return noeud
def contient(self, valeur):
noeud = self.racine
while noeud is not None:
if valeur == noeud.valeur:
return True
noeud = noeud.gauche if valeur < noeud.valeur else noeud.droit
return False
def parcours_infixe(self):
resultat = []
self._infixe(self.racine, resultat)
return resultat
def _infixe(self, noeud, acc):
if noeud is not None:
self._infixe(noeud.gauche, acc)
acc.append(noeud.valeur)
self._infixe(noeud.droit, acc)
arbre = ABR()
for v in [5, 3, 8, 1, 4, 7]:
arbre.inserer(v)
print(arbre.contient(4)) # True
print(arbre.parcours_infixe()) # [1, 3, 4, 5, 7, 8]
Le parcours infixe d'un ABR restitue les valeurs dans l'ordre croissant.