Adloun

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.

Définition 9.1Classe

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.

Définition 9.2Objet et instance

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

Définition 9.3Attribut

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.

Définition 9.4Méthode

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.

Exemple 9.5Une classe `Point` complète

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 .

Proposition 9.6Attribut d'instance et attribut de classe

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

Définition 9.7Encapsulation

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.
Exemple 9.8Encapsulation d'un compte bancaire

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 par print et str.
  • __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 par repr.

Si __str__ n'est pas défini, Python utilise __repr__ comme solution de repli.

Exemple 9.9Représentation textuelle d'un point

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
Proposition 9.10Quelques autres méthodes spéciales

De nombreuses méthodes spéciales existent. Les plus utiles en NSI sont :

  • __len__(self) : appelée par len(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

Définition 9.11Hé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.

Exemple 9.12Animaux et héritage

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

Définition 9.13Pile

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é.

Exemple 9.14Classe `Pile`

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

Définition 9.15File

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).

Exemple 9.16Classe `File`

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

Définition 9.17Arbre 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.

Exemple 9.18Classes `Noeud` et `ABR`

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.

Continuer sur Adloun : animation, QCM, fiches, exercices