Chapitre 10

La programmation orientée objet

Introduction

Pour traiter un problème, il existe deux approches principales :

  • Approche procédurale : basée sur des fonctions indépendantes des données. Chaque fonction est associée à un traitement et peut être réutilisée dans différents contextes.
  • Approche orientée objet (POO) : basée sur des objets qui regroupent données et comportements. Le code est découpé en entités autonomes appelées classes. Les données sont protégées par encapsulation, ce qui permet de mieux modéliser la réalité.
Encapsulation

L'encapsulation regroupe les données (attributs) et les fonctions qui les manipulent (méthodes) dans une même entité : la classe. L'instance d'une classe est appelée objet.

  • Une classe est équivalente à un nouveau type de données (comme int ou list).
  • On peut créer autant d'objets (instances) que l'on souhaite à partir d'une même classe.
  • Les classes Python existantes (int, list, dict…) suivent ce même principe.

Classes et Objets

Pour créer une classe, on précise son nom, ses attributs (ce qui la décrit) et ses méthodes (ce qu'elle peut faire).

Exemple — Classe CompteBancaire :

  • Attributs : numCompte, solde, dteOuverture, typeCompte
  • Méthodes : ouvrir(), fermer(), depot(), retrait()
Attributs de classe vs attributs d'instance
  • Attributs de classe : définis dans le corps de la classe, en dehors de toute méthode. Partagés par toutes les instances.
  • Attributs d'instance : définis dans le constructeur via self.nom. Propres à chaque objet, créés et détruits avec lui.

Exemple — Classe Time1 avec attributs de classe :

Python
class Time1:
    """Classe représentant une heure."""
    h = 0   # attribut de classe (partagé par toutes les instances)
    m = 0
    s = 0

    def modifierHeure(self, heure):
        if 0 <= heure <= 23:
            self.h = heure
        else:
            raise ValueError("L'heure doit être entre 0 et 23")

    def afficheTime(self):
        print(str(self.h) + ":" + str(self.m) + ":" + str(self.s))

t = Time1()
t.afficheTime()       # 0:0:0
t.modifierHeure(21)
t.afficheTime()       # 21:0:0

q = Time1()
q.afficheTime()       # 0:0:0  (attributs de classe inchangés)

Exemple — Classe Time2 avec constructeur paramétré :

Python
class Time2:
    """Classe représentant une heure (attributs d'instance)."""

    def __init__(self, heure=0, minute=0, seconde=0):
        self.h = heure    # attribut d'instance
        self.m = minute
        self.s = seconde

    def afficheTime(self):
        print(str(self.h) + ":" + str(self.m) + ":" + str(self.s))

q = Time2(12, 25, 10)
q.afficheTime()   # 12:25:10

# Time2.h  →  AttributeError : pas d'attribut de classe h

Les méthodes spéciales

Les méthodes spéciales (ou dunder methods) portent des noms prédéfinis entourés de doubles tirets bas. Elles permettent de personnaliser le comportement des objets avec les opérateurs Python.

Principales méthodes spéciales
  • __init__(self, ...) — Constructeur : initialise un nouvel objet.
  • __repr__(self) — Représentation officielle, appelée par repr() et dans la console.
  • __str__(self) — Représentation lisible, appelée par print() et str().
  • __add__(self, autre) — Surcharge de + : permet d'écrire obj1 + obj2.
  • __sub__(self, autre) — Surcharge de -.
  • __mul__(self, autre) — Surcharge de *.
  • __truediv__(self, autre) — Surcharge de /.
  • __eq__(self, autre) — Surcharge de ==.
  • __len__(self) — Surcharge de len().
  • __getitem__(self, i) — Surcharge de obj[i].
  • __call__(self, ...) — Surcharge de l'appel obj(...).

L'héritage

L'héritage permet de créer une sous-classe (classe dérivée) qui hérite des attributs et méthodes d'une classe parente (superclasse), et les étend ou les redéfinit.

Héritage en Python
  • La classe fille hérite de tous les attributs et méthodes de la classe mère.
  • On peut ajouter de nouvelles méthodes ou attributs.
  • On peut redéfinir (surcharger) des méthodes héritées.
  • super() appelle le constructeur ou une méthode de la classe parente.
  • Syntaxe : class SousClasse(ClasseMere):

Exemple — Géométrie : Polygone est la classe de base ; Rectangle en hérite, et Carre hérite de Rectangle.

Python
class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    def distance(self, p):
        return ((self.x - p.x)**2 + (self.y - p.y)**2)**0.5
    def __repr__(self):
        return 'Point(' + str(self.x) + ',' + str(self.y) + ')'
    def __str__(self):
        return '(' + str(self.x) + ',' + str(self.y) + ')'

class Polygone:
    def __init__(self, listPoints):
        self.sommets = listPoints
        self.nom = "Polygone"
    def perimetre(self):
        p = 0
        for i in range(len(self.sommets) - 1):
            p += self.sommets[i].distance(self.sommets[i + 1])
        p += self.sommets[0].distance(self.sommets[-1])
        return p
    def __repr__(self):
        return self.nom + ":" + "-->".join([str(s) for s in self.sommets])

class Rectangle(Polygone):          # hérite de Polygone
    def __init__(self, listPoints):
        super().__init__(listPoints)
        self.nom = "Rectangle"
    def longueur(self):
        return self.sommets[0].distance(self.sommets[1])
    def largeur(self):
        return self.sommets[1].distance(self.sommets[2])

class Carre(Rectangle):             # hérite de Rectangle
    def __init__(self, listPoints):
        super().__init__(listPoints)
        self.nom = "Carre"

# Utilisation
A = Point(0, 0); B = Point(4, 0); C = Point(4, 3); D = Point(0, 3)
r = Rectangle([A, B, C, D])
print(r)               # Rectangle:(0,0)-->(4,0)-->(4,3)-->(0,3)
print(r.perimetre())   # 14.0
print(r.longueur())    # 4.0

Exercices avec corrigés

Exercice 1 — La classe Cplx → Voir le corrigé

L'objectif est de créer une classe Python pour la manipulation des nombres complexes. On l'appellera Cplx.

Le constructeur reçoit la partie réelle x et la partie imaginaire y. Les attributs sont re et im. Ainsi z = Cplx(1, -3) crée le complexe z = 1 − 3i.

Définir dans la classe Cplx les méthodes suivantes :

  • __init__(self, x, y) — constructeur.
  • __repr__(self) — affiche le complexe sous la forme a+bi.
  • __add__(self, autre) — addition de deux complexes (z1 + z2).
  • module(self) — retourne le module.
  • conjugue(self) — retourne le conjugué.
  • inverse(self) — retourne l'inverse.
  • __sub__(self, autre) — soustraction (z1 - z2).
  • __mul__(self, autre) — multiplication (z1 * z2).
  • __truediv__(self, autre) — division (z1 / z2).
  • __eq__(self, autre) — égalité (z1 == z2).
Exercice 2 — La classe Poly → Voir le corrigé

Construire la classe Poly pour créer et manipuler des polynômes à coefficients réels. Les attributs seront :

  • self.c : liste des coefficients non nuls.
  • self.d : liste des degrés associés (dans l'ordre décroissant).

Exemple : P = Poly([-5, 2, 1], [4, 3, 0]) représente −5x⁴ + 2x³ + 1.

Redéfinir : __init__, __repr__, __add__, __sub__, __eq__.

Définir :

  • eval(x) — valeur du polynôme pour un réel x.
  • deriv() — polynôme dérivé.
  • integrale(a, b) — intégrale entre a et b.
Exercice 3 — La classe Rationnel → Voir le corrigé

Concevoir une classe Rationnel pour manipuler des nombres rationnels. Les rationnels calculés doivent toujours être des fractions irréductibles.

  • __init__(self, n=0, d=1) — crée le rationnel n/d.
  • setNumer(n), setDenom(d) — modifient les valeurs.
  • __repr__(self) — affiche sous forme de fraction (ex : 3/4).
  • __add__, __mul__, __truediv__, __eq__.
  • inv() — inverse du rationnel.
  • pgcd(a, b) — PGCD de deux entiers.
  • irreductible() — simplifie la fraction.
Exercice 4 — PolynomeCreux (Concours MP/PC/PT 2017) → Voir le corrigé

Un polynôme creux est un polynôme dont certains coefficients sont nuls. Un monôme axn est représenté par un dictionnaire {n: a}. Un polynôme creux est une association de monômes stockée dans l'attribut data.

Compléter le squelette de la classe PolynomeCreux :

  • Q1ajout_monome(monome={}) : ajoute un monôme saisi au clavier si monome est vide, sinon ajoute le monôme passé en argument.
  • Q2degree() : retourne le degré du polynôme.
  • Q3__call__(self, x0) : valeur du polynôme pour x0.
  • Q4__add__(self, other) : somme (sans monômes nuls).
  • Q5__mul__(self, other) : produit (sans monômes nuls).
  • Q6__str__(self) : chaîne ordonnée par degrés décroissants. Ex : "6*x**12 + x**9 - x**7 + 4".
  • Q7primitive() : primitive du polynôme (constante nulle).
  • Q8integrale(a, b) : valeur de l'intégrale entre a et b.
Python
class PolynomeCreux:
    """Manipulation des polynômes creux à une seule variable."""
    def __init__(self):
        self.data = {}          # polynôme nul

    def ajout_monome(self, monome={}):
        if len(monome) == 0:
            # Q1 : saisir degré et coefficient au clavier
            ...
        else:
            degre = list(monome.keys())[0]
            self.data[degre] = list(monome.values())[0]

    def degree(self):   ...     # Q2
    def __call__(self, x0): ... # Q3
    def __add__(self, other): ...  # Q4
    def __mul__(self, other): ...  # Q5
    def __str__(self): ...         # Q6
    def primitive(self): ...       # Q7
    def integrale(self, a, b): ... # Q8
Exercice 5 — Point, Segment, Cercle, Rectangle → Voir le corrigé

Classe Point : attributs x, y.

  • distance(point) — distance entre deux points.
  • translate(dx, dy) — translate le point.
  • isobarycentre(points) — isobarycentre d'un ensemble de points.
  • Redéfinir __repr__ et __eq__.

Classe Segment : attributs point1, point2.

  • longueur() — déléguer le calcul à Point.distance().
  • milieu() — retourne le point milieu.
  • Redéfinir __repr__ et __eq__.

Classe Cercle : attributs centre (Point) et rayon.

  • Redéfinir __repr__"Cercle de centre (x,y) et de rayon r".
  • getPerimetre(), getSurface().
  • appartient(point), interieur(point).

Classe Rectangle : attributs coin_haut_gauche et coin_bas_droite (Points).

  • getPerimetre() et getSurface().
Exercice 6 — La classe VectorOfInt → Voir le corrigé

Écrire la classe VectorOfInt qui gère un tableau à capacité dynamique. Elle contient un tableau d'entiers TE et un entier size. Fournir les méthodes :

  • ensureCapacity(c) — si capacité < c, nouvelle capacité = max(c, 2 × capacité actuelle). Nouvelles cases à 0.
  • resize(s) — modifie la taille, augmente la capacité si nécessaire.
  • getSize() — taille actuelle.
  • isEmpty()True si vide.
  • add(e) — ajoute un élément à la fin.
  • set(i, e) — modifie l'élément à la position i.
  • get(i) — retourne l'élément à la position i (None si hors bornes).
  • sum(), max(), indexMax(), sort().
Exercice 7 — La classe StackOfInt → Voir le corrigé

Écrire la classe StackOfInt qui gère une pile d'entiers en s'appuyant sur un objet de type VectorOfInt. Fournir :

  • push(i) — empile un entier.
  • peek() — retourne l'entier en sommet sans le dépiler.
  • pop() — dépile et retourne l'entier en sommet.
  • size() — nombre d'entiers dans la pile.
  • isEmpty()True si la pile est vide.
Corrigé Exercice 1 ← Retour à l'exercice
Python
from math import sqrt

class Cplx:
    def __init__(self, x, y):
        self.re = x
        self.im = y

    def __repr__(self):
        return str(self.re) + '+' + str(self.im) + 'i'

    def __add__(self, autre):
        return Cplx(self.re + autre.re, self.im + autre.im)

    def module(self):
        return sqrt(self.re**2 + self.im**2)

    def conjugue(self):
        return Cplx(self.re, -self.im)

    def inverse(self):
        m2 = self.module()**2
        return Cplx(self.re / m2, -self.im / m2)

    def __sub__(self, autre):
        return Cplx(self.re - autre.re, self.im - autre.im)

    def __mul__(self, autre):
        re = self.re * autre.re - self.im * autre.im
        im = self.re * autre.im + self.im * autre.re
        return Cplx(re, im)

    def __truediv__(self, autre):
        return self * autre.inverse()

    def __eq__(self, autre):
        return self.re == autre.re and self.im == autre.im

# Exemples
z1 = Cplx(1, 2)
z2 = Cplx(3, -4)
print(z1)            # 1+2i
print(z1 + z2)       # 4+-2i
print(z1 * z2)       # 11+2i
print(z1.module())   # 2.2360679...
print(z1 == z2)      # False
print(z1 / z2)       # appelle z1 * z2.inverse()
Corrigé Exercice 2 ← Retour à l'exercice
Python
class Poly:
    def __init__(self, coeff, degre):
        self.c = coeff    # coefficients non nuls
        self.d = degre    # degrés associés (ordre décroissant)

    def __repr__(self):
        termes = [str(self.c[i]) + 'x^' + str(self.d[i])
                  for i in range(len(self.c))]
        return ' + '.join(termes)

    def __add__(self, autre):
        i, j = 0, 0
        Lc, Ld = [], []
        while i < len(self.d) and j < len(autre.d):
            if self.d[i] > autre.d[j]:
                Ld.append(self.d[i]); Lc.append(self.c[i]); i += 1
            elif self.d[i] < autre.d[j]:
                Ld.append(autre.d[j]); Lc.append(autre.c[j]); j += 1
            else:
                s = self.c[i] + autre.c[j]
                if s != 0:
                    Ld.append(self.d[i]); Lc.append(s)
                i += 1; j += 1
        while i < len(self.d):
            Ld.append(self.d[i]); Lc.append(self.c[i]); i += 1
        while j < len(autre.d):
            Ld.append(autre.d[j]); Lc.append(autre.c[j]); j += 1
        return Poly(Lc, Ld)

    def __sub__(self, autre):
        neg = Poly([-x for x in autre.c], autre.d[:])
        return self + neg

    def __eq__(self, autre):
        return self.c == autre.c and self.d == autre.d

    def eval(self, x):
        return sum(self.c[i] * x**self.d[i] for i in range(len(self.c)))

    def deriv(self):
        Lc = [self.c[i] * self.d[i] for i in range(len(self.c)) if self.d[i] != 0]
        Ld = [self.d[i] - 1 for i in range(len(self.d)) if self.d[i] != 0]
        return Poly(Lc, Ld)

    def integrale(self, a, b):
        prim_c = [self.c[i] / (self.d[i] + 1) for i in range(len(self.c))]
        prim_d = [self.d[i] + 1 for i in range(len(self.d))]
        P = Poly(prim_c, prim_d)
        return P.eval(b) - P.eval(a)

# Exemple
P = Poly([-5, 2, 1], [4, 3, 0])
print(P)          # -5x^4 + 2x^3 + 1
print(P.eval(1))  # -2
print(P.deriv())  # -20x^3 + 6x^2
Corrigé Exercice 3 ← Retour à l'exercice
Python
class Rationnel:
    def __init__(self, n=0, d=1):
        self.numer = n
        self.denom = d
        self.irreductible()

    def pgcd(self, a, b):
        while b != 0:
            a, b = b, a % b
        return a

    def irreductible(self):
        if self.numer == 0:
            return
        p = self.pgcd(abs(self.numer), abs(self.denom))
        self.numer //= p
        self.denom //= p

    def setNumer(self, n):
        self.numer = n
        self.irreductible()

    def setDenom(self, d):
        self.denom = d
        self.irreductible()

    def __repr__(self):
        if self.numer == 0:
            return "0"
        if self.denom == 1:
            return str(self.numer)
        return str(self.numer) + "/" + str(self.denom)

    def __add__(self, autre):
        n = self.numer * autre.denom + self.denom * autre.numer
        return Rationnel(n, self.denom * autre.denom)

    def __mul__(self, autre):
        return Rationnel(self.numer * autre.numer, self.denom * autre.denom)

    def __truediv__(self, autre):
        return self * autre.inv()

    def __eq__(self, autre):
        return self.numer == autre.numer and self.denom == autre.denom

    def inv(self):
        return Rationnel(self.denom, self.numer)

# Utilisation
r1 = Rationnel(1, 2)
r2 = Rationnel(1, 3)
print(r1)          # 1/2
print(r1 + r2)     # 5/6
print(r1 * r2)     # 1/6
print(r1 / r2)     # 3/2
Corrigé Exercice 4 ← Retour à l'exercice
Python
class PolynomeCreux:
    """Manipulation des polynômes creux à une seule variable."""

    def __init__(self):
        self.data = {}

    def ajout_monome(self, monome={}):
        if len(monome) == 0:
            # Q1 : saisie au clavier avec validations
            while True:
                n = int(input("degré = "))
                if n >= 0:
                    break
            while True:
                a = float(input("coefficient = "))
                if a != 0:
                    break
            self.data[n] = a
        else:
            n = list(monome.keys())[0]
            self.data[n] = list(monome.values())[0]

    def degree(self):
        # Q2
        return max(self.data.keys())

    def __call__(self, x0):
        # Q3
        return sum(self.data[c] * x0**c for c in self.data)

    def __add__(self, other):
        # Q4
        p = PolynomeCreux()
        all_deg = set(self.data.keys()) | set(other.data.keys())
        for d in all_deg:
            c = self.data.get(d, 0) + other.data.get(d, 0)
            if c != 0:
                p.ajout_monome({d: c})
        return p

    def __mul__(self, other):
        # Q5
        p = PolynomeCreux()
        for d1 in self.data:
            for d2 in other.data:
                d = d1 + d2
                c = self.data[d1] * other.data[d2]
                if d in p.data:
                    p.data[d] += c
                else:
                    p.data[d] = c
        p.data = {d: c for d, c in p.data.items() if c != 0}
        return p

    def __str__(self):
        # Q6
        if not self.data:
            return "0"
        L = sorted(self.data.keys(), reverse=True)
        termes = []
        for d in L:
            c = self.data[d]
            if d == 0:
                termes.append(str(c))
            elif d == 1:
                termes.append(str(c) + '*x')
            else:
                termes.append(str(c) + '*x**' + str(d))
        return ' + '.join(termes)

    def primitive(self):
        # Q7
        p = PolynomeCreux()
        for d in self.data:
            p.ajout_monome({d + 1: self.data[d] / (d + 1)})
        return p

    def integrale(self, a, b):
        # Q8
        return self.primitive()(b) - self.primitive()(a)

# Exemple
P = PolynomeCreux()
P.ajout_monome({2: 3})
P.ajout_monome({0: -1})
print(P)                  # 3*x**2 + -1
print(P(2))               # 11.0
print(P.integrale(0, 1))  # 2.0
Corrigé Exercice 5 ← Retour à l'exercice
Python
from math import pi, sqrt

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

    def distance(self, point):
        return sqrt((self.x - point.x)**2 + (self.y - point.y)**2)

    def translate(self, dx, dy):
        self.x += dx
        self.y += dy

    def isobarycentre(self, points):
        tous = [self] + points
        mx = sum(p.x for p in tous) / len(tous)
        my = sum(p.y for p in tous) / len(tous)
        return Point(mx, my)

    def __repr__(self):
        return 'Point(' + str(self.x) + ', ' + str(self.y) + ')'

    def __eq__(self, autre):
        return self.x == autre.x and self.y == autre.y


class Segment:
    def __init__(self, point1, point2):
        self.point1 = point1
        self.point2 = point2

    def longueur(self):
        return self.point1.distance(self.point2)

    def milieu(self):
        mx = (self.point1.x + self.point2.x) / 2
        my = (self.point1.y + self.point2.y) / 2
        return Point(mx, my)

    def __repr__(self):
        return 'Segment(' + repr(self.point1) + ', ' + repr(self.point2) + ')'

    def __eq__(self, autre):
        return ((self.point1 == autre.point1 and self.point2 == autre.point2) or
                (self.point1 == autre.point2 and self.point2 == autre.point1))


class Cercle:
    def __init__(self, centre, rayon):
        self.centre = centre
        self.rayon = rayon

    def __repr__(self):
        return ('Cercle de centre (' + str(self.centre.x) + ',' +
                str(self.centre.y) + ') et de rayon ' + str(self.rayon))

    def getPerimetre(self):
        return 2 * pi * self.rayon

    def getSurface(self):
        return pi * self.rayon**2

    def appartient(self, point):
        return self.centre.distance(point) == self.rayon

    def interieur(self, point):
        return self.centre.distance(point) < self.rayon


class Rectangle:
    def __init__(self, coin_haut_gauche, coin_bas_droite):
        self.chg = coin_haut_gauche
        self.cbd = coin_bas_droite

    def getPerimetre(self):
        l = abs(self.cbd.x - self.chg.x)
        h = abs(self.cbd.y - self.chg.y)
        return 2 * (l + h)

    def getSurface(self):
        l = abs(self.cbd.x - self.chg.x)
        h = abs(self.cbd.y - self.chg.y)
        return l * h

# Exemples
A = Point(0, 0); B = Point(3, 4)
print(A.distance(B))            # 5.0
s = Segment(A, B)
print(s.longueur())             # 5.0
print(s.milieu())               # Point(1.5, 2.0)
c = Cercle(Point(0, 0), 5)
print(c)                        # Cercle de centre (0,0) et de rayon 5
r = Rectangle(Point(0, 4), Point(3, 0))
print(r.getPerimetre())         # 14
print(r.getSurface())           # 12
Corrigé Exercice 6 ← Retour à l'exercice
Python
class VectorOfInt:

    def __init__(self, capacite=0):
        self.TE = [0] * capacite
        self.size = 0

    def ensureCapacity(self, c):
        if len(self.TE) < c:
            m = max(c, 2 * len(self.TE)) if len(self.TE) > 0 else c
            self.TE = self.TE + [0] * (m - len(self.TE))

    def __repr__(self):
        return ', '.join(str(self.TE[i]) for i in range(self.size))

    def resize(self, s):
        if len(self.TE) < s:
            self.ensureCapacity(s)
        self.size = s

    def getSize(self):
        return self.size

    def isEmpty(self):
        return self.size == 0

    def add(self, e):
        self.size += 1
        self.ensureCapacity(self.size)
        self.TE[self.size - 1] = e

    def set(self, i, e):
        if i < self.size:
            self.TE[i] = e

    def get(self, i):
        if i < self.size:
            return self.TE[i]
        return None

    def __getitem__(self, i):
        return self.get(i)

    def sum(self):
        return sum(self.TE[:self.size])

    def max(self):
        return max(self.TE[:self.size])

    def indexMax(self):
        return self.TE[:self.size].index(self.max())

    def sort(self):
        A = self.TE[:self.size]
        A.sort()
        self.TE[:self.size] = A

# Utilisation
v = VectorOfInt()
v.add(5); v.add(2); v.add(8); v.add(1)
print(v)             # 5, 2, 8, 1
print(v.max())       # 8
print(v.indexMax())  # 2
v.sort()
print(v)             # 1, 2, 5, 8
Corrigé Exercice 7 ← Retour à l'exercice
Python
class StackOfInt:

    def __init__(self):
        self.v = VectorOfInt()

    def push(self, i):
        self.v.add(i)

    def peek(self):
        if self.v.getSize() == 0:
            return None
        return self.v[self.v.getSize() - 1]

    def pop(self):
        x = self.peek()
        if x is not None:
            self.v.resize(self.v.getSize() - 1)
        return x

    def size(self):
        return self.v.getSize()

    def isEmpty(self):
        return self.v.isEmpty()

# Utilisation
stack = StackOfInt()
stack.push(10)
stack.push(20)
stack.push(30)
print(stack.peek())  # 30
print(stack.pop())   # 30
print(stack.size())  # 2