backend / python
POO : héritage et polymorphisme
Explication
Ce que vous allez apprendre
- Créer une hiérarchie de classes avec
class Chien(Animal)et réutiliser du code sans duplication - Utiliser
super()pour appeler l'implémentation d'une méthode de la classe parente - Comprendre le polymorphisme : appeler la même méthode sur des objets de types différents
- Lire un MRO (
.__mro__) pour résoudre un conflit d'héritage multiple - Savoir quand préférer la composition ("a un") à l'héritage ("est un")
Dans quel contexte ?
Un développeur maintient un système de facturation avec une classe Facture et se retrouve, après plusieurs mois, avec FactureAvecRemise, FactureAvecRemiseEtTaxe, FactureProfessionnelleAvecRemise... une hiérarchie d'héritage qui devient impossible à faire évoluer sans tout casser. Cette leçon montre pourquoi remplacer cette pyramide par une composition (Facture qui possède une liste de Remise et de Taxe appliquées) est souvent la sortie de secours la plus saine.
Réutiliser sans copier-coller
L'héritage permet à une classe de reprendre automatiquement les attributs et méthodes d'une autre, puis d'en modifier ou compléter certains. C'est un outil puissant pour éviter la duplication : au lieu de réécrire se_presenter() dans Chien et dans Chat, on l'écrit une seule fois dans Animal et chaque sous-classe se contente de personnaliser parler().
Le polymorphisme, ou la même consigne comprise différemment
Le polymorphisme est cette capacité à appeler la même méthode sur des objets de types différents et à obtenir un comportement adapté à chacun, sans avoir besoin de savoir à l'avance de quel type précis il s'agit. Parcourir une liste mélangeant des Chien et des Chat et appeler .se_presenter() sur chacun illustre parfaitement l'idée : le code appelant n'a pas besoin de tester le type de chaque objet.
Quand plusieurs parents entrent en conflit
L'héritage multiple pose une question naturelle : si deux classes parentes définissent la même méthode, laquelle est utilisée ? Python répond avec un algorithme précis, le MRO (Method Resolution Order), consultable via .__mro__. C'est un mécanisme rarement nécessaire à comprendre en détail, mais utile à savoir expliquer le jour où un bug d'héritage multiple surgit.
| Question à se poser | Si la réponse est oui |
|---|---|
| "X est un Y" (un Chien est un Animal) | Héritage : class Chien(Animal) |
| "X possède un Y" (une Voiture a un Moteur) | Composition : self.moteur = Moteur() |
| Plusieurs comportements à combiner librement | Composition, pour éviter une hiérarchie rigide |
Bonne pratique
Avant d'écrire class B(A), demandez-vous si la phrase "B est un A" reste vraie dans tous les cas. Si la relation ressemble plutôt à "B utilise un A" ou "B a besoin d'un A", préférez la composition : elle reste flexible même quand les besoins évoluent.
Composition : une alternative souvent plus saine
L'héritage répond à la question est-ce que X est une sorte de Y, la composition répond à est-ce que X possède un Y. Une voiture n'est pas un moteur, mais elle en a un. Cette leçon insiste volontairement sur ce point : privilégier la composition évite des hiérarchies de classes rigides et difficiles à faire évoluer, un piège classique de la POO mal maîtrisée.
Commandes & code
POO : héritage et polymorphisme
Réutiliser et spécialiser du comportement entre classes.
class Animal:
def __init__(self, nom: str):
self.nom = nom
def parler(self) -> str:
raise NotImplementedError("Les sous-classes doivent implementer parler()")
def se_presenter(self) -> str:
return f"Je suis {self.nom} et je fais : {self.parler()}"
class Chien(Animal):
def parler(self) -> str:
return "Wouf !"
class Chat(Animal):
def parler(self) -> str:
return "Miaou !"
# Polymorphisme : meme interface, comportements differents selon le type reel
animaux = [Chien("Rex"), Chat("Felix"), Chien("Buddy")]
for animal in animaux:
print(animal.se_presenter()) # appelle la bonne methode selon le type
# super() : appeler l'implementation de la classe parente
class Employe:
def __init__(self, nom: str, salaire: float):
self.nom = nom
self.salaire = salaire
def decrire(self) -> str:
return f"{self.nom} : {self.salaire} EUR/mois"
class Manager(Employe):
def __init__(self, nom: str, salaire: float, equipe: list):
super().__init__(nom, salaire) # reutilise l'init du parent
self.equipe = equipe
def decrire(self) -> str:
base = super().decrire() # reutilise la logique du parent
return f"{base} (manage {len(self.equipe)} personnes)"
# Heritage multiple et MRO (Method Resolution Order)
class Volant:
def deplacer(self):
return "vole dans les airs"
class Nageur:
def deplacer(self):
return "nage dans l'eau"
class CanardMagique(Volant, Nageur):
pass
c = CanardMagique()
print(c.deplacer()) # "vole dans les airs" : Volant est avant Nageur dans le MRO
print(CanardMagique.__mro__) # ordre de resolution des methodes (algorithme C3)
# Classes abstraites : imposer un contrat aux sous-classes
from abc import ABC, abstractmethod
class FormeGeometrique(ABC):
@abstractmethod
def aire(self) -> float: ...
@abstractmethod
def perimetre(self) -> float: ...
def resume(self) -> str:
# methode concrete disponible pour toutes les sous-classes
return f"Aire={self.aire():.2f}, Perimetre={self.perimetre():.2f}"
class Rectangle(FormeGeometrique):
def __init__(self, largeur, hauteur):
self.largeur = largeur
self.hauteur = hauteur
def aire(self) -> float:
return self.largeur * self.hauteur
def perimetre(self) -> float:
return 2 * (self.largeur + self.hauteur)
# FormeGeometrique() # TypeError : impossible d'instancier une classe abstraite
rect = Rectangle(4, 5)
print(rect.resume())
# isinstance / issubclass : verifications polymorphiques
print(isinstance(rect, FormeGeometrique)) # True
print(issubclass(Rectangle, FormeGeometrique)) # True
# Composition plutot qu'heritage : souvent preferable ("favor composition over inheritance")
class Moteur:
def demarrer(self):
return "Vroom"
class Voiture:
def __init__(self):
self.moteur = Moteur() # composition : Voiture "a" un Moteur
def demarrer(self):
return self.moteur.demarrer()Résumé
super()appelle l'implémentation de la classe parente sans la nommer explicitement.- L'ordre de résolution des méthodes (MRO) suit l'algorithme C3 en cas d'héritage multiple.
ABC+@abstractmethodempêchent l'instanciation directe et imposent un contrat.- La composition ("a un") est souvent préférable à l'héritage ("est un") pour la flexibilité.
Exercices pratiques
Mission : trancher un conflit d'héritage multiple sur un canard magique
Objectif : Prédire le comportement d'un héritage multiple via le MRO, puis refactorer une hiérarchie de factures rigide en composition.
Contexte
Une classe CanardMagique(Volant, Nageur) hérite de deux classes qui définissent chacune une méthode deplacer() différente. Un développeur est persuadé que Python va lever une erreur d'ambiguïté, mais le code s'exécute sans problème et retourne un résultat précis.
Tu dois expliquer comment Python résout ce conflit, refactorer une hiérarchie de facturation devenue ingérable en composition, puis raisonner sur les classes abstraites.