Exercices sur la programmation orientée objet

Exercice 1. QCM d’activation

Pour chaque question, une seule réponse est correcte.

Question 1. On considère le code suivant :

class Chien:
    def __init__(self, nom, age):
        self.nom = nom
        self.age = age

rex = Chien("Rex", 5)

Dans ce code, rex est :

  • A) une classe
  • B) un attribut
  • C) une instance (objet)
  • D) une méthode
Correction

Réponse C. rex est une instance (un objet) de la classe Chien, créé par l’appel du constructeur Chien("Rex", 5).

  • A est faux : Chien est la classe, pas rex ;
  • B est faux : nom et age sont des attributs ;
  • D est faux : une méthode est une fonction définie dans la classe (comme __init__).

Question 2. Quel est le rôle du paramètre self dans une méthode ?

  • A) C’est un mot-clé obligatoire de Python
  • B) Il fait référence à la classe elle-même
  • C) Il fait référence à l’objet sur lequel la méthode est appelée
  • D) Il contient la valeur de retour de la méthode
Correction

Réponse C. Quand on écrit rex.aboyer(), Python passe automatiquement rex comme valeur de self. C’est une référence vers l’objet courant.

  • A est faux : self n’est pas un mot-clé, c’est une convention de nommage ;
  • B est faux : self désigne l’objet (l’instance), pas la classe ;
  • D est faux : la valeur de retour est donnée par return.

Question 3. On considère :

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

p = Point(3, 7)

Que vaut p.x + p.y ?

  • A) une erreur car on ne peut pas additionner des attributs
  • B) (3, 7)
  • C) 10
  • D) "37"
Correction

Réponse C. p.x vaut 3 et p.y vaut 7, donc p.x + p.y vaut 10. La notation pointée permet d’accéder aux attributs d’un objet.

  • A est faux : les attributs sont des entiers, l’addition est définie ;
  • B est faux : ce serait le cas pour (p.x, p.y) ;
  • D est faux : ce serait le cas si x et y étaient des chaînes "3" et "7".

Question 4. Que se passe-t-il si on oublie self dans la définition d’une méthode ?

class Voiture:
    def __init__(self, marque):
        self.marque = marque

    def klaxonner():     # self oublié
        return "Bip !"

v = Voiture("Renault")
v.klaxonner()
  • A) La méthode affiche "Bip !"
  • B) Python lève une erreur TypeError
  • C) La méthode retourne None
  • D) Python ignore le self manquant
Correction

Réponse B. Python passe automatiquement l’objet v comme premier argument lors de l’appel v.klaxonner(), mais la méthode n’attend aucun paramètre. Cela provoque TypeError: klaxonner() takes 0 positional arguments but 1 was given.

  • A est faux : l’erreur survient avant l’exécution du corps de la méthode ;
  • C est faux : la méthode n’est jamais exécutée ;
  • D est faux : Python ne corrige pas cette erreur automatiquement.

Question 5. Quelle méthode spéciale faut-il définir pour que print(obj) affiche une représentation lisible d’un objet ?

  • A) __init__
  • B) __add__
  • C) __repr__
  • D) __eq__
Correction

Réponse C. La méthode __repr__ (ou __str__) est appelée automatiquement par print() pour obtenir une représentation en chaîne de l’objet.

  • A est faux : __init__ sert à initialiser l’objet lors de sa création ;
  • B est faux : __add__ est appelée par l’opérateur + ;
  • D est faux : __eq__ est appelée par l’opérateur ==.

Exercice 2. Classe Rectangle (guidé)

On souhaite créer une classe Rectangle pour représenter des rectangles géométriques.

Partie A. Compléter le code suivant :

class Rectangle:
    def __init__(self, longueur, largeur):
        self.________ = longueur
        self.________ = largeur

    def perimetre(self):
        return ________________________

    def surface(self):
        return ________________________

    def __repr__(self):
        return f"Rectangle({self.longueur}, {self.largeur})"

Partie B. Tester la classe en créant un rectangle de longueur 7 et de largeur 5, puis en affichant son périmètre et sa surface.

Partie C (enrichissement). L’héritage permet de créer une classe fille qui hérite des attributs et méthodes d’une classe mère.

Créer une classe Parallelepipede qui hérite de Rectangle et possède en plus un attribut hauteur et une méthode volume(). Le constructeur devra appeler celui de Rectangle avec Rectangle.__init__(self, longueur, largeur).

Correction

Partie A.

class Rectangle:
    def __init__(self, longueur, largeur):
        self.longueur = longueur
        self.largeur = largeur

    def perimetre(self):
        return 2 * (self.longueur + self.largeur)

    def surface(self):
        return self.longueur * self.largeur

    def __repr__(self):
        return f"Rectangle({self.longueur}, {self.largeur})"

On initialise les attributs avec self.longueur et self.largeur. Le périmètre d’un rectangle est $2 \times (L + l)$ et sa surface est $L \times l$.

Partie B.

r = Rectangle(7, 5)
print(r)             # Rectangle(7, 5)
print(r.perimetre()) # 24
print(r.surface())   # 35

Partie C.

class Parallelepipede(Rectangle):
    def __init__(self, longueur, largeur, hauteur):
        Rectangle.__init__(self, longueur, largeur)
        self.hauteur = hauteur

    def volume(self):
        return self.longueur * self.largeur * self.hauteur

p = Parallelepipede(7, 5, 2)
print(p.perimetre()) # 24 (méthode héritée de Rectangle)
print(p.volume())    # 70

La classe Parallelepipede hérite de Rectangle : elle dispose automatiquement des méthodes perimetre() et surface() sans les redéfinir. L’appel Rectangle.__init__(self, longueur, largeur) dans le constructeur permet d’initialiser les attributs hérités.

Remarque : l’héritage n’est pas exigible au baccalauréat. Il est présenté ici à titre d’enrichissement.

Exercice 3. Classe Compte bancaire (guidé)

On souhaite modéliser un compte bancaire par une classe CompteBancaire.

  1. Écrire le constructeur __init__ qui prend en paramètres numero_compte (entier), nom (chaîne du propriétaire) et solde (nombre), et les stocke comme attributs d’instance.

  2. Écrire une méthode versement(montant) qui ajoute montant au solde.

  3. Écrire une méthode retrait(montant) qui retire montant du solde.

  4. Écrire une méthode agios() qui applique des agios de 5 % : le solde est multiplié par 0.95.

  5. Écrire une méthode __repr__ qui retourne une chaîne de la forme "Compte n°1234 (Marcel) : 300 euros".

  6. Tester la classe avec le scénario suivant : créer un compte n°1234 pour Marcel avec un solde de 300 euros, effectuer un retrait de 500 euros, un versement de 2000 euros, puis appliquer les agios. Afficher le compte après chaque opération.

Correction
class CompteBancaire:

    def __init__(self, numero_compte, nom, solde):
        self.numero = numero_compte
        self.nom = nom
        self.solde = solde

    def versement(self, montant):
        self.solde += montant

    def retrait(self, montant):
        self.solde -= montant

    def agios(self):
        self.solde = self.solde * 0.95

    def __repr__(self):
        return f"Compte n°{self.numero} ({self.nom}) : {self.solde} euros"

On teste le scénario :

c = CompteBancaire(1234, "Marcel", 300)
print(c)  # Compte n°1234 (Marcel) : 300 euros

c.retrait(500)
print(c)  # Compte n°1234 (Marcel) : -200 euros

c.versement(2000)
print(c)  # Compte n°1234 (Marcel) : 1800 euros

c.agios()
print(c)  # Compte n°1234 (Marcel) : 1710.0 euros

Chaque méthode modifie l’attribut self.solde de l’objet. La méthode __repr__ est appelée automatiquement par print(), ce qui permet un affichage lisible sans écrire de méthode afficher() dédiée.

Remarquons que le solde peut devenir négatif (retrait de 500 sur un solde de 300). On pourrait améliorer la méthode retrait en vérifiant que le montant ne dépasse pas le solde, mais ce n’est pas demandé ici.

Exercice 4. Classe Cercle

On souhaite modéliser un cercle $C(O, r)$ de centre $O(a, b)$ et de rayon $r$.

  1. Définir une classe Cercle avec un constructeur prenant en paramètres les coordonnées a, b du centre et le rayon r.

  2. Écrire une méthode surface() qui retourne la surface du cercle ($\pi r^2$). On importera pi depuis le module math.

  3. Écrire une méthode perimetre() qui retourne le périmètre du cercle ($2\pi r$).

  4. Écrire une méthode contient(x, y) qui retourne True si le point $A(x, y)$ est situé à l’intérieur du cercle (c’est-à-dire si $\sqrt{(x - a)^2 + (y - b)^2} \leqslant r$), et False sinon.

  5. Tester avec le cercle $C$ de centre $(1, 2)$ et de rayon $3$. Le point $(2, 3)$ est-il à l’intérieur ? Et le point $(5, 5)$ ?

Correction
from math import pi, sqrt

class Cercle:
    def __init__(self, a, b, r):
        self.a = a
        self.b = b
        self.r = r

    def surface(self):
        return pi * self.r ** 2

    def perimetre(self):
        return 2 * pi * self.r

    def contient(self, x, y):
        distance = sqrt((x - self.a) ** 2 + (y - self.b) ** 2)
        return distance <= self.r

    def __repr__(self):
        return f"Cercle(centre=({self.a}, {self.b}), rayon={self.r})"

Test :

c = Cercle(1, 2, 3)
print(c.surface())       # 28.274333882308138
print(c.perimetre())     # 18.84955592153876
print(c.contient(2, 3))  # True
print(c.contient(5, 5))  # False

Pour le point $(2, 3)$ : $\sqrt{(2-1)^2 + (3-2)^2} = \sqrt{1 + 1} = \sqrt{2} \approx 1.41$, ce qui est inférieur à $3$, donc le point est à l’intérieur.

Pour le point $(5, 5)$ : $\sqrt{(5-1)^2 + (5-2)^2} = \sqrt{16 + 9} = \sqrt{25} = 5$, ce qui est supérieur à $3$, donc le point est à l’extérieur.

Exercice 5. Classe Chronomètre

On souhaite créer une classe Chronometre représentant un temps en heures, minutes et secondes.

  1. Écrire le constructeur prenant en paramètres h, m et s (avec des valeurs par défaut à 0).

  2. Écrire la méthode __repr__ qui retourne le temps au format "02h15m30s" (chaque valeur sur deux chiffres).

    Aide : l’expression f"{valeur:02d}" formate un entier sur deux chiffres avec un zéro initial si nécessaire.

  3. Écrire une méthode en_secondes() qui retourne le temps total converti en secondes.

  4. Écrire une méthode avancer(s) qui ajoute s secondes au chronomètre. Le chronomètre devra gérer les dépassements (60 secondes = 1 minute, 60 minutes = 1 heure).

    Aide : on peut convertir le temps total en secondes, ajouter s, puis reconvertir avec des divisions entières // et des modulos %.

  5. Écrire la méthode __add__ pour pouvoir additionner deux chronomètres avec l’opérateur +.

  6. Tester : créer un chronomètre à 1h45m50s, avancer de 75 secondes, puis additionner avec un chronomètre à 0h30m20s.

Correction
class Chronometre:
    def __init__(self, h=0, m=0, s=0):
        self.h = h
        self.m = m
        self.s = s

    def __repr__(self):
        return f"{self.h:02d}h{self.m:02d}m{self.s:02d}s"

    def en_secondes(self):
        return self.h * 3600 + self.m * 60 + self.s

    def avancer(self, s):
        total = self.en_secondes() + s
        self.h = total // 3600
        self.m = (total % 3600) // 60
        self.s = total % 60

    def __add__(self, other):
        total = self.en_secondes() + other.en_secondes()
        h = total // 3600
        m = (total % 3600) // 60
        s = total % 60
        return Chronometre(h, m, s)

Test :

c1 = Chronometre(1, 45, 50)
print(c1)            # 01h45m50s

c1.avancer(75)
print(c1)            # 01h47m05s

Vérification : $1 \times 3600 + 45 \times 60 + 50 = 6350$ secondes. On ajoute 75 : $6350 + 75 = 6425$ secondes. $6425 \div 3600 = 1$ heure (reste $2825$). $2825 \div 60 = 47$ minutes (reste $5$). Résultat : 01h47m05s.

c2 = Chronometre(0, 30, 20)
c3 = c1 + c2
print(c3)            # 02h17m25s

Vérification : $6425 + 1820 = 8245$ secondes. $8245 \div 3600 = 2$ heures (reste $1045$). $1045 \div 60 = 17$ minutes (reste $25$). Résultat : 02h17m25s.

La méthode __add__ crée et retourne un nouvel objet Chronometre sans modifier les objets existants, contrairement à avancer qui modifie l’objet sur lequel elle est appelée. Cette distinction entre méthode mutatrice et méthode qui retourne un nouvel objet est importante en POO.

Exercice 6. Classe myString (synthèse)

Les chaînes de caractères Python (str) sont immuables : on ne peut pas les modifier après création. On souhaite créer une classe myString qui encapsule une chaîne et lui ajoute des méthodes de modification similaires à celles des listes.

  1. Écrire le constructeur qui prend une chaîne en paramètre et la stocke dans un attribut s.

  2. Écrire la méthode __repr__ qui retourne la chaîne stockée entourée de guillemets simples, par exemple 'hello'.

  3. Écrire une méthode append(texte) qui concatène texte à la fin de la chaîne stockée. La méthode ne retourne rien (elle modifie l’objet).

  4. Écrire une méthode pop(i) qui supprime le caractère à l’indice i et renvoie ce caractère (comme list.pop).

    Aide : on peut reconstruire la chaîne par concaténation de la partie avant l’indice et de la partie après l’indice.

  5. Écrire une méthode __len__ pour que len(obj) retourne la longueur de la chaîne stockée.

  6. Écrire une méthode __eq__ pour pouvoir comparer deux objets myString avec ==.

  7. Tester :

s1 = myString("hello")
s1.append(" world")
print(s1)             # 'hello world'
print(len(s1))        # 11
s1.pop(5)
print(s1)             # 'helloworld'
s2 = myString("helloworld")
print(s1 == s2)       # True
Correction
class myString:
    def __init__(self, s):
        self.s = s

    def __repr__(self):
        return f"'{self.s}'"

    def append(self, texte):
        self.s = self.s + texte

    def pop(self, i):
        caractere = self.s[i]
        self.s = self.s[:i] + self.s[i + 1:]
        return caractere

    def __len__(self):
        return len(self.s)

    def __eq__(self, other):
        if isinstance(other, myString):
            return self.s == other.s
        return False

Test :

s1 = myString("hello")
s1.append(" world")
print(s1)             # 'hello world'
print(len(s1))        # 11

L’appel s1.append(" world") modifie l’attribut self.s en le remplaçant par "hello" + " world", soit "hello world".

c = s1.pop(5)
print(c)              # ' ' (le caractère à l'indice 5 est l'espace)
print(s1)             # 'helloworld'

La méthode pop extrait le caractère à l’indice 5 (l’espace), puis reconstruit la chaîne sans ce caractère : "hello" + "world" = "helloworld".

s2 = myString("helloworld")
print(s1 == s2)       # True

La méthode __eq__ vérifie d’abord que other est bien une instance de myString (avec isinstance), puis compare les chaînes internes. Cela évite les erreurs si on compare un myString avec un objet d’un autre type.

Cet exercice illustre le principe d’encapsulation : on enrichit le comportement d’un type existant en l’enveloppant dans une classe avec de nouvelles méthodes, tout en gardant la donnée interne (self.s) comme seule source de vérité.