Retour au cours

backend / python

POO : héritage et polymorphisme

Leçon 121 exercice

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 poserSi 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 librementComposition, 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.

python
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 + @abstractmethod empê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

1 disponible
1

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.

Résoudre l’exercice →