Exercices : 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})"
Indice

Les attributs portent le nom des paramètres du constructeur, précédé de self.. Dans une méthode, on les utilise de la même façon : le périmètre vaut 2 × (longueur + largeur) et la surface longueur × 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.

Indice

Créer l’objet avec Rectangle(7, 5) et le ranger dans une variable, puis appeler ses méthodes avec la notation pointée, par exemple r.perimetre().

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

Indice

La classe fille se déclare avec class Parallelepipede(Rectangle):. Son constructeur reçoit les trois dimensions, appelle Rectangle.__init__ pour la longueur et la largeur, puis crée self.hauteur. Le volume vaut longueur × largeur × hauteur.

Code à copier puis compléter
class Parallelepipede(Rectangle):
    def __init__(self, longueur, largeur, hauteur):
        Rectangle.__init__(self, ..., ...)
        self.hauteur = ...

    def volume(self):
        return ...
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.

    Indice

    Un attribut par paramètre, créé avec self. : par exemple self.solde = solde.

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

    Indice

    La méthode modifie l’attribut du solde : self.solde = self.solde + montant. Elle ne renvoie rien.

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

    Indice

    Même principe que versement, avec une soustraction.

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

    Indice

    Multiplier self.solde par 0.95 et ranger le résultat dans self.solde.

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

    Indice

    __repr__ renvoie une f-string construite avec les attributs du compte, sur le modèle f"Compte n°{...} ({...}) : {...} 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.

    Indice

    Créer le compte avec CompteBancaire(1234, "Marcel", 300), puis appeler chaque méthode et afficher le compte avec print après chaque opération : print utilise __repr__.

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.

    Indice

    Trois attributs, un par paramètre : self.a, self.b et self.r.

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

    Indice

    Écrire from math import pi en haut du programme, puis traduire la formule avec l’attribut du rayon, self.r. Le carré s’écrit ** 2.

  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.

    Indice

    Calculer la distance entre le point et le centre avec sqrt (à importer aussi depuis math), puis renvoyer directement le résultat de la comparaison distance <= self.r, qui vaut True ou False.

  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)$ ?

    Indice

    Créer c = Cercle(1, 2, 3), puis afficher c.contient(2, 3) et c.contient(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).

    Indice

    Les valeurs par défaut s’écrivent dans la ligne de définition : def __init__(self, h=0, m=0, s=0):. Ranger ensuite chaque paramètre dans un attribut.

  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.

    Indice

    Une heure vaut 3600 secondes et une minute 60 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 %.

    Code à copier puis compléter
        def avancer(self, s):
            total = self.en_secondes() + s
            self.h = total // ...
            self.m = (total % ...) // ...
            self.s = total % ...
    
  5. Écrire la méthode __add__ pour pouvoir additionner deux chronomètres avec l’opérateur +.

    Indice

    __add__(self, other) reçoit le second chronomètre. Additionner les deux temps en secondes, convertir le total en heures, minutes et secondes comme dans avancer, puis construire et renvoyer un nouveau Chronometre : les deux chronomètres de départ ne changent pas.

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

    Indice

    c1 = Chronometre(1, 45, 50), puis c1.avancer(75) ; l’expression c1 + Chronometre(0, 30, 20) appelle __add__. Afficher le résultat avec print.

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

    Indice

    __repr__ renvoie une chaîne : construire une f-string qui place self.s entre deux guillemets simples.

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

    Indice

    Concaténer avec + et ranger le résultat dans self.s.

  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.

    Code à copier puis compléter
        def pop(self, i):
            caractere = self.s[...]
            self.s = self.s[:...] + self.s[...:]
            return caractere
    
  5. Écrire une méthode __len__ pour que len(obj) retourne la longueur de la chaîne stockée.

    Indice

    len(obj) appelle obj.__len__() : renvoyer la longueur de la chaîne stockée dans l’attribut.

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

    Indice

    __eq__(self, other) reçoit l’objet comparé : deux myString sont égaux lorsque leurs attributs s sont égaux.

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