Apprenez en lisant dans l'ordre

Polymorphisme — Même nom de méthode, comportement différent par type

Apprends le polymorphisme Python. Surcharge les méthodes du parent dans les sous-classes pour que les appelants n'aient pas à réfléchir aux types, et remplace les branches if type(...) par de la POO propre — le tout avec des schémas.

La dernière fois on a couvert l'héritage multiple et le MRO. Pour clore la série POO, cet article couvre le troisième pilier — le polymorphisme.

C'est quoi le polymorphisme ?

Le polymorphisme est l'idée que la même interface (nom de méthode) peut faire des choses différentes selon le type. Suppose que tu veux une seule opération « calculer le salaire », mais que tu veux que les employés, les managers et les ingénieurs utilisent des formules différentes.

Définis calculate_salary sur la classe parente Employee, et fais en sorte que les sous-classes Manager et Engineer la surchargent avec leur propre formule. Maintenant le code appelant écrit juste employee.calculate_salary() sans se soucier de quelle classe est laquelle, et le bon calcul tourne.

Trois classes surchargent calculate_salary
Employee(parent)base_salary(inchangé)= 300kManager(surcharge enfant)base +team x 50k= 1,2MEngineer(surcharge enfant)base +skill x 20k= 380k
Le parent Employee définit calculate_salary. Les sous-classes Manager / Engineer la surchargent avec leurs propres formules. Même nom de méthode, résultats différents par classe.

Bâtir sur le parent, changer la formule dans chaque enfant

Construisons l'exemple du salaire pour de vrai. Employee est le parent, avec Manager (boost selon la taille de l'équipe) et Engineer (boost selon le niveau de compétence) comme enfants. Le point clé est que les trois définissent une méthode du même nom calculate_salary.

class Employee:
    def __init__(self, name, base_salary):
        self.name        = name
        self.base_salary = base_salary

    def calculate_salary(self):                  # défaut = salaire de base seulement
        return self.base_salary


class Manager(Employee):
    def __init__(self, name, base_salary, team_size):
        super().__init__(name, base_salary)
        self.team_size = team_size

    def calculate_salary(self):                  # bonus selon taille de l'équipe
        return self.base_salary + self.team_size * 50000


class Engineer(Employee):
    def __init__(self, name, base_salary, skill_level):
        super().__init__(name, base_salary)
        self.skill_level = skill_level

    def calculate_salary(self):                  # bonus selon niveau de compétence
        return self.base_salary + self.skill_level * 20000

Définis Employee / Manager / Engineer et confirme que chaque calculate_salary retourne un résultat différent.

① Construis les trois classes selon l'exemple ci-dessus (Manager et Engineer doivent appeler super().__init__(name, base_salary) pour déléguer l'init du parent).

② Construis Employee("Alice", 300000), Manager("Léa", 800000, 8) et Engineer("Hugo", 300000, 4), puis print chaque résultat de calculate_salary().

(Une fois que ça tourne correctement, l'explication apparaîtra.)

Éditeur Python

Exécuter le code pour voir le résultat
Comportement de emp.calculate_salary() dans une boucle
emp.calculate_salary()(même ligne)emp = alice(Employee)-> 300kemp = lea(Manager)-> 1,2Memp = hugo(Engineer)-> 380kEmployeeManagerEngineer
Le corps de la boucle est juste emp.calculate_salary() sur une seule ligne, mais une méthode différente est choisie automatiquement selon la classe de emp — c'est la puissance du polymorphisme.

Rassembler dans une liste — Une boucle qui se fiche des types

Le polymorphisme montre vraiment sa force quand tu mets des objets de types différents dans une seule liste et les traites tous ensemble. Enveloppe le calcul de salaire dans une classe PayrollSystem et le corps de la boucle se réduit à une seule ligne.

class PayrollSystem:
    def __init__(self):
        self.employees = []

    def add(self, employee):
        self.employees.append(employee)

    def total(self):
        result = 0
        for emp in self.employees:                       # les types peuvent varier
            result += emp.calculate_salary()              # même nom fonctionne
        return result


payroll = PayrollSystem()
payroll.add(Employee("Alice", 300000))
payroll.add(Manager("Léa",  800000, 8))
payroll.add(Engineer("Hugo", 300000, 4))

print(payroll.total())   # 1880000
PayrollSystem se fiche de ce qui est dedans
PayrollSystem
  • employees = [...] (types mélangés)
  • for emp in self.employees: les parcourt
  • Appelle juste emp.calculate_salary()
Employee (par défaut)
  • retourne base_salary
Manager
  • base + team * 50k
Engineer
  • base + skill * 20k
Même avec trois classes différentes mélangées dans la liste, PayrollSystem appelle simplement le même nom de méthode et le bon calcul tourne par type.

Réutilise les trois classes de la Pratique 1 pour construire un PayrollSystem qui calcule le salaire total (la console garde l'état, donc les définitions de classes précédentes sont toujours là).

① Définis class PayrollSystem: avec __init__ qui initialise self.employees = []. Implémente add(self, employee) et total(self) (que total parcourt une boucle for en sommant chaque emp.calculate_salary()).

② Construis payroll = PayrollSystem(), puis add trois employés : Employee("Alice", 300000) / Manager("Léa", 800000, 8) / Engineer("Hugo", 300000, 4).

③ Boucle sur payroll.employees en imprimant emp.name et emp.calculate_salary() ligne par ligne, puis imprime payroll.total() avec le préfixe "total :".

Éditeur Python

Exécuter le code pour voir le résultat

Ce que ça donne sans polymorphisme

Essaie de faire la même chose sans polymorphisme et tu finis à écrire des branches if type(emp) == ...:. Ça fonctionne, mais chaque nouveau rôle veut dire une autre branche if — et le moment où tu en oublies une, tu as un bug.

# (MAUVAIS) sans polymorphisme (branchement par type)
def calc(emp):
    if type(emp) is Manager:
        return emp.base_salary + emp.team_size * 50000
    elif type(emp) is Engineer:
        return emp.base_salary + emp.skill_level * 20000
    else:
        return emp.base_salary


# (BON) avec polymorphisme (logique poussée dans les classes)
def calc(emp):
    return emp.calculate_salary()         # une ligne
Branches par type vs polymorphisme
MAUVAIS : par typeif type ==x N foischaque nouveau type= nouvelle brancheBON : polymorphiqueemp.calculate()nouveau type =juste une classeondulecontenu
Le code branchement-par-type empile des if type sur l'appelant et grossit avec chaque nouveau type. Le polymorphisme garde l'appelant sur une seule ligne — ajouter un nouveau type signifie juste ajouter une classe.

« L'appelant ne se soucie pas des types » — c'est le mantra

Un test fiable pour savoir si ton design est polymorphique : est-ce que le code de l'appelant a des piles de if type(...) ou if isinstance(...) ? Si oui, le refactor standard est de déplacer ce branchement dans des surcharges de méthodes sur les classes. « Ajouter une classe » et « ajouter un if » ont tendance à être un compromis.

Duck typing — Tout ce qu'il faut, c'est le même nom de méthode

Le polymorphisme de Python a aussi une version plus permissive : le duck typing. Le dicton « si ça marche comme un canard et que ça cancane comme un canard, c'est un canard » se transforme en « si une classe a la bonne méthode, peu importe quelle classe c'est ».

Dans l'exemple ci-dessous, Cat et Dog ne partagent pas du tout un parent Animal — mais tant que les deux ont une méthode speak(), la même fonction gère les deux. Python privilégie « est-ce qu'elle a la méthode ? » sur le graphe d'héritage.

class Cat:
    def speak(self):
        return "Miaou"

class Dog:
    def speak(self):
        return "Ouaf"

def shout(animal):                # le type n'est pas imposé
    print(animal.speak())

shout(Cat())   # Miaou
shout(Dog())   # Ouaf

Des langages comme Java ou C# exigent une classe parente partagée pour que le polymorphisme fonctionne. Python est content tant que la méthode est là au moment de l'appel. Ça te donne de la flexibilité, mais « s'assurer que les noms de méthodes ont la même signification entre classes » devient la responsabilité de l'appelant — garde ça en tête.

Les deux designs sur une seule diapo

Design polymorphique vs design avec branchement par type
Design polymorphique
  • Code de l'appelant — une ligne emp.calculate_salary()
  • Ajouter un nouveau type — juste écrire une nouvelle classe avec calculate_salary
  • Rayon d'impact — reste à l'intérieur des classes
  • Lisibilité — « même nom, varie par type » suffit à suivre
Design avec branchement par type
  • Code de l'appelant — pile de if type(emp) is ...
  • Ajouter un nouveau type — chaque branche doit être revue
  • Rayon d'impact — déborde sur l'appelant
  • Lisibilité — tu relis les branches à chaque fois
Même besoin, deux designs — mais combien de code l'appelant écrit et jusqu'où les changements ondulent peut varier énormément.
QUIZ

Vérification des connaissances

Répondez à chaque question une par une.

Question 1Quelle est la description la plus précise du polymorphisme ?

Question 2Le polymorphisme tend à éliminer quelle structure du code de l'appelant ?

Question 3Quelle est la meilleure description du duck typing de Python ?