Le polymorphisme : une interface commune, des comportements différents
- Expliquer ce qu’est le polymorphisme en termes simples et avec un exemple
- Écrire du code qui fonctionne sur des objets de types différents via une interface commune, sans vérifier le type
- Comprendre pourquoi le polymorphisme remplace les chaînes de if/elif selon le type
- Connaître le Duck Typing — le polymorphisme sans héritage commun
Qu’allons-nous apprendre ?
- Ce qu’est le polymorphisme (Polymorphism).
- Une interface commune (Common Interface) : la même méthode, un comportement différent pour chaque type.
- Pourquoi il remplace le
if type == .... - Le Duck Typing : la version souple de Python.
Pourquoi en avons-nous besoin ?
Supposons qu’un système de paiement doive débiter un client, et que le client puisse payer par carte bancaire, par PayPal ou par virement bancaire. Sans polymorphisme :
def pay(method, amount):
if method["type"] == "credit":
print("Charging card", method["card"], amount)
elif method["type"] == "paypal":
print("Redirecting", method["email"], "to PayPal for", amount)
elif method["type"] == "bank":
print("Waiting for transfer of", amount, "to account", method["account"])Et que se passe-t-il quand on ajoute Apple Pay ? Il faut ajouter un elif ici, mais aussi dans la fonction de remboursement, et dans le rapport mensuel, et dans la vérification des frais… Chaque fonction qui gère des paiements contient la même chaîne de if. On en oublie une, et il y a un bug.
Dans la première leçon de POO, c’était le « problème 4 ». Le polymorphisme en est la solution.
Explication à partir des bases
Qu’est-ce que le polymorphisme ?
Le polymorphisme (du grec : « plusieurs formes ») = la capacité de travailler avec des objets de types différents via la même interface, où chaque objet effectue l’action à sa façon.
En termes simples : le même appel (method.pay(100)) → un comportement différent, selon le type de l’objet. Le code appelant n’a pas besoin de savoir quel type il détient.
La télécommande a un bouton « marche ». Vous appuyez dessus face à une télévision, et elle s’allume. Face à un climatiseur, il commence à refroidir. Face à une enceinte, elle commence à jouer. Le même bouton (une interface), une action différente pour chaque appareil. Et vous, qui tenez la télécommande, n’avez pas besoin de savoir comment chaque appareil fonctionne à l’intérieur. Un nouvel appareil arrive ? S’il « comprend » le bouton, la télécommande fonctionne avec lui sans aucun changement.
Comment ça marche : une interface commune
- Dans le parent, définissez l’interface : quelles méthodes ont tous les types (
pay,refund,fee). - Chaque enfant redéfinit les méthodes à sa façon.
- Le code qui les utilise travaille face à l’interface :
method.pay(amount), et non face aux types.
Nous l’avons déjà vu ! Dans le describe() d’Employee, qui appelait self.monthly_cost() et atteignait la bonne version d’un manager ou d’un stagiaire.
L’avantage principal : Open/Closed
Avec le polymorphisme, ajouter un nouveau type (Apple Pay) = écrire une seule nouvelle classe. Pas une seule ligne du code existant qui utilise les paiements ne change. Le système est ouvert à l’extension et fermé à la modification, un principe important de la conception logicielle.
Le Duck Typing
En Python, le polymorphisme n’exige pas l’héritage. Python ne vérifie pas « l’objet hérite-t-il de PaymentMethod ? ». Il essaie simplement d’appeler la méthode. Si la méthode existe, cela fonctionne.
« Si ça marche comme un canard et que ça cancane comme un canard, c’est un canard. »
Le Duck Typing = « peu importe ce que tu es, ce qui compte, c’est ce que tu sais faire ». Nous l’avons déjà utilisé : len() fonctionne sur str, list, dict et set, car ils ont tous une « longueur ». for fonctionne sur tout ce que l’on peut parcourir.
Et pourtant, hériter d’un parent commun est la façon recommandée quand on conçoit une interface volontairement : cela documente ce qu’est l’interface, et permet du code partagé.
Syntaxe
class Base:
def action(self): # the common interface
raise NotImplementedError("subclasses must implement action()")
class TypeA(Base):
def action(self):
return "A does it this way"
class TypeB(Base):
def action(self):
return "B does it another way"
for item in [TypeA(), TypeB()]: # polymorphic code:
print(item.action()) # one call, many behaviors, no if/elifraise NotImplementedError dans le parent = « chaque enfant doit implémenter cette méthode ». Si un enfant a oublié, vous obtenez une erreur claire au moment de l’appel.
Premier exemple de code
Le système de paiement, avec le polymorphisme. week-3/payments.py :
class PaymentMethod:
def __init__(self, owner):
self.owner = owner
def pay(self, amount):
raise NotImplementedError("subclasses must implement pay()")
def fee(self, amount):
return 0
class CreditCard(PaymentMethod):
def __init__(self, owner, last_digits):
super().__init__(owner)
self.last_digits = last_digits
def pay(self, amount):
return "Charged " + str(amount) + " to card ****" + self.last_digits
def fee(self, amount):
return round(amount * 0.02, 2)
class PayPal(PaymentMethod):
def __init__(self, owner, email):
super().__init__(owner)
self.email = email
def pay(self, amount):
return "Sent " + str(amount) + " via PayPal from " + self.email
class BankTransfer(PaymentMethod):
def pay(self, amount):
return "Waiting for a bank transfer of " + str(amount) + " from " + self.owner
def checkout(method, amount):
total = amount + method.fee(amount)
print(method.pay(total), "| fee:", method.fee(amount))
methods = [CreditCard("Dana", "4321"), PayPal("Avi", "avi@mail.com"), BankTransfer("Noa")]
for method in methods:
checkout(method, 100)Sortie :
Charged 102.0 to card ****4321 | fee: 2.0
Sent 100 via PayPal from avi@mail.com | fee: 0
Waiting for a bank transfer of 100 from Noa | fee: 0Analyse du code ligne par ligne
| Code | Explication |
|---|---|
class PaymentMethod | L’interface : chaque moyen de paiement doit avoir pay, et a fee (0 par défaut). |
raise NotImplementedError | Une « méthode abstraite » : elle définit une interface, sans implémentation. |
CreditCard.pay, PayPal.pay, BankTransfer.pay | Chacun implémente pay à sa façon. |
CreditCard.fee | Une redéfinition : des frais de 2 %. Les autres héritent du 0 du parent. |
def checkout(method, amount): | Le code polymorphe : il ne vérifie pas le type, il ne contient aucun if. Il appelle seulement method.fee() et method.pay(). |
| La boucle | Exactement la même fonction, trois comportements différents. |
Un autre exemple
Ajouter un nouveau type — sans toucher au code existant. Puis le Duck Typing.
class ApplePay(PaymentMethod):
def __init__(self, owner, device):
super().__init__(owner)
self.device = device
def pay(self, amount):
return "Paid " + str(amount) + " with Apple Pay on " + self.device
def fee(self, amount):
return 1
checkout(ApplePay("Tom", "iPhone 15"), 100)Paid 101 with Apple Pay on iPhone 15 | fee: 1checkout n’a pas du tout changé. Nous avons ajouté une classe, et cela fonctionne. Dans la version if/elif, il aurait fallu modifier chaque fonction qui gère des paiements.
Le Duck Typing — un objet qui n’hérite pas de PaymentMethod :
class GiftCard:
def __init__(self, code):
self.code = code
def pay(self, amount):
return "Redeemed " + str(amount) + " from gift card " + self.code
def fee(self, amount):
return 0
checkout(GiftCard("GIFT-2026"), 100)Redeemed 100 from gift card GIFT-2026 | fee: 0GiftCard n’hérite pas de PaymentMethod, mais il a pay et fee, donc checkout fonctionne avec lui. C’est le Duck Typing. (Dans un vrai projet, il vaut quand même mieux qu’il hérite, pour que l’interface soit documentée et explicite.)
Un autre exemple familier : les notifications
class Notification:
def __init__(self, recipient):
self.recipient = recipient
def send(self, text):
raise NotImplementedError
class EmailNotification(Notification):
def send(self, text):
return "Email to " + self.recipient + ": " + text
class SmsNotification(Notification):
def send(self, text):
return "SMS to " + self.recipient + ": " + text[:20]
def notify_all(channels, text):
for channel in channels:
print(channel.send(text))
notify_all([EmailNotification("dana@mail.com"), SmsNotification("050-1234567")],
"Your order #1042 has shipped and will arrive tomorrow")Email to dana@mail.com: Your order #1042 has shipped and will arrive tomorrow
SMS to 050-1234567: Your order #1042 hasChaque canal a ses propres règles (le SMS est coupé à 20 caractères), et notify_all ne le sait pas et ne s’en soucie pas.
Erreur courante
Erreur 1 : vérifier le type au lieu d’utiliser le polymorphisme.
def checkout(method, amount):
if isinstance(method, CreditCard):
...
elif isinstance(method, PayPal):
...Cela nous ramène exactement au problème de départ, simplement avec des classes. Si vous vous surprenez à écrire if isinstance(...) sur vos propres classes pour décider comment effectuer une action, c’est le signe que l’action devrait être une méthode que chaque classe implémente.
Erreur 2 : oublier d’implémenter une méthode abstraite.
class Cash(PaymentMethod):
pass
checkout(Cash("Rina"), 50)NotImplementedError: subclasses must implement pay()Et c’est une bonne chose : l’erreur est claire et indique exactement ce qui manque. Sans le raise dans le parent, vous obtiendriez un comportement silencieux et faux, ou None au lieu d’une chaîne.
Erreur 3 : une interface incohérente entre les types.
class CreditCard(PaymentMethod):
def pay(self, amount, cvv):
...Maintenant, checkout (qui appelle method.pay(total)) échoue uniquement sur les cartes bancaires. Toutes les implémentations doivent recevoir les mêmes paramètres. Une information propre à un type (comme un CVV) va dans le __init__ ou dans un attribut de ce type.
Erreur 4 : croire que le polymorphisme exige l’héritage. En Python, non (le Duck Typing). Mais hériter d’une base commune documente l’interface, et évite les erreurs. Préférez-le quand vous concevez.
Exercices
Les exercices n’utilisent que ce qui a été vu dans cette leçon et les précédentes. Essayez d’abord seul, et n’ouvrez la solution qu’ensuite.
Ce que vous pratiquez sur cette page : le polymorphisme — le même appel pour plusieurs types, où chaque type se comporte différemment ; ajouter un nouveau type sans modifier le code existant ; le Duck Typing et NotImplementedError.
À la fin de la page, vous saurez :
- Remplacer une chaîne de
ifselon le type par une méthode partagée que chaque type implémente - Ajouter un nouveau type sans toucher au code qui l’utilise
- Expliquer ce qu’est le Duck Typing
- Obliger les enfants à implémenter une méthode grâce à
NotImplementedError
Exercice 1: À la place d’une chaîne de if
Ce que vous construisez : des moyens de paiement à la caisse — sans vérifier dans le code le type de chacun.
Le code que vous remplacez :
def pay(method, amount):
if method["type"] == "credit":
return "Charging card " + str(amount)
elif method["type"] == "paypal":
return "Redirecting to PayPal " + str(amount)Ce qu’il faut écrire :
- Une classe de base nommée
PaymentMethod, avecpayqui reçoit un montant et lanceNotImplementedError. - Deux enfants :
CreditCardetPayPal. Chacun redéfinitpayet renvoie sa propre phrase de la sortie. - Une fonction nommée
checkoutqui reçoit un moyen de paiement et un montant, et appellepay— sansifsur le type.
Code de test :
for method in [CreditCard(), PayPal()]:
print(checkout(method, 100))La sortie souhaitée :
Charging card 100
Redirecting to PayPal 100Afficher la solution
class PaymentMethod:
def pay(self, amount):
raise NotImplementedError("subclasses must implement pay()")
class CreditCard(PaymentMethod):
def pay(self, amount):
return "Charging card " + str(amount)
class PayPal(PaymentMethod):
def pay(self, amount):
return "Redirecting to PayPal " + str(amount)
def checkout(method, amount):
return method.pay(amount)
for method in [CreditCard(), PayPal()]:
print(checkout(method, 100))Charging card 100
Redirecting to PayPal 100checkout ne sait pas, et n’a pas besoin de savoir, de quel type de paiement il s’agit — il appelle seulement pay. Chaque type l’implémente différemment. C’est le polymorphisme.
Exercice 2: Un nouveau type — sans toucher à checkout
Ce que vous construisez : la suite de la caisse — l’ajout d’un nouveau moyen de paiement et de frais.
Ce qu’il faut écrire :
- Un nouvel enfant nommé
ApplePay, avecpayqui renvoie sa propre phrase de la sortie. - Dans
PaymentMethod, ajoutezfeequi reçoit un montant et renvoie 0 — une valeur par défaut pour tous. - Dans
CreditCard, redéfinissezfee: des frais de 2 % du montant. - Mettez à jour
checkoutpour qu’il renvoie aussi les frais, comme dans la sortie.
Code de test :
for method in [CreditCard(), PayPal(), ApplePay()]:
print(checkout(method, 100))La sortie souhaitée :
Charging card 100 | fee: 2.0
Redirecting to PayPal 100 | fee: 0
Paid with Apple Pay 100 | fee: 0Afficher la solution
class PaymentMethod:
def pay(self, amount):
raise NotImplementedError("subclasses must implement pay()")
def fee(self, amount):
return 0
class CreditCard(PaymentMethod):
FEE_PERCENT = 2
def pay(self, amount):
return "Charging card " + str(amount)
def fee(self, amount):
return amount * CreditCard.FEE_PERCENT / 100
class PayPal(PaymentMethod):
def pay(self, amount):
return "Redirecting to PayPal " + str(amount)
class ApplePay(PaymentMethod):
def pay(self, amount):
return "Paid with Apple Pay " + str(amount)
def checkout(method, amount):
return method.pay(amount) + " | fee: " + str(method.fee(amount))
for method in [CreditCard(), PayPal(), ApplePay()]:
print(checkout(method, 100))Charging card 100 | fee: 2.0
Redirecting to PayPal 100 | fee: 0
Paid with Apple Pay 100 | fee: 0ApplePay a été ajouté sans changer une seule ligne de checkout. PayPal et ApplePay héritent du fee du parent, et CreditCard le redéfinit.
Exercice 3: Des personnages dans un jeu
Ce que vous construisez : un combat entre des personnages d’un jeu — où chaque type de personnage a une puissance d’attaque différente.
Ce qu’il faut écrire :
- Une classe de base nommée
Character, avec un nom et une santé, etattack_powerqui lanceNotImplementedError. Warrior— sa puissance d’attaque vaut toujours 15.Mage— sa puissance d’attaque vaut 10, plus 10 s’il a plus de 50 de santé.- Une fonction nommée
attackqui reçoit un attaquant et une cible : elle retire de la santé à la cible selon la puissance de l’attaquant, et affiche une phrase comme dans la sortie.
Code de test :
conan = Warrior("Conan", 100)
merlin = Mage("Merlin", 60)
attack(conan, merlin)
attack(merlin, conan)
print(merlin.name, merlin.health)
print(conan.name, conan.health)La sortie souhaitée :
Conan hits Merlin for 15
Merlin hits Conan for 10
Merlin 45
Conan 90Afficher la solution
class Character:
def __init__(self, name, health):
self.name = name
self.health = health
def attack_power(self):
raise NotImplementedError("subclasses must implement attack_power()")
class Warrior(Character):
def attack_power(self):
return 15
class Mage(Character):
STRONG_FROM = 50
def attack_power(self):
if self.health > Mage.STRONG_FROM:
return 20
return 10
def attack(attacker, target):
power = attacker.attack_power()
target.health = target.health - power
print(attacker.name, "hits", target.name, "for", power)
conan = Warrior("Conan", 100)
merlin = Mage("Merlin", 60)
attack(conan, merlin)
attack(merlin, conan)
print(merlin.name, merlin.health)
print(conan.name, conan.health)Conan hits Merlin for 15
Merlin hits Conan for 10
Merlin 45
Conan 90Après l’attaque de Conan, il ne restait à Merlin que 45 de santé — moins de 50 — sa puissance est donc tombée à 10. attack ne sait rien des règles de chaque personnage.
Exercice 4: Le Duck Typing
Ce que vous faites : lire du code et expliquer — sans écrire de code.
Le code :
class Duck:
def speak(self):
return "Quack"
class Robot:
def speak(self):
return "Beep"
def make_it_speak(thing):
return thing.speak()La tâche :
DucketRobotn’héritent pas du même parent.make_it_speakfonctionnera-t-elle avec les deux ? Pourquoi ?- Expliquez en une phrase ce qu’est le Duck Typing.
Afficher la solution
- Oui.
make_it_speakappelle seulementspeak()— elle ne vérifie pas le type. Tout objet qui aspeakconvient. - Le Duck Typing : « si ça marche comme un canard et que ça cancane comme un canard — c’est un canard ». En Python, ce qui compte, c’est que la méthode existe, et non de quelle classe vient l’objet. Une classe de base commune reste utile — pour définir l’interface à un seul endroit et donner un comportement par défaut — mais elle n’est pas obligatoire.
Exercice 5: NotImplementedError
Ce que vous construisez : des formes qui savent chacune calculer leur propre aire — et un parent qui l’exige.
Le code :
class Shape:
def area(self):
raise NotImplementedError("subclasses must implement area()")
class Square(Shape):
def __init__(self, side):
self.side = sideLa tâche :
- Que se passera-t-il quand vous appellerez l’aire de
Square(3)? Pourquoi est-ce en fait une bonne chose ? - Corrigez
Squarepour qu’il calcule une aire. - Ajoutez
Circlequi reçoit un rayon, et calcule l’aire avecmath.pi, arrondie à deux chiffres. - Affichez dans une boucle l’aire d’un carré de côté 3 et d’un cercle de rayon 3.
La sortie souhaitée :
9
28.27Afficher la solution
- Une
NotImplementedError: subclasses must implement area()sera lancée. C’est une bonne chose : l’erreur est découverte immédiatement et clairement — celui qui a écritSquarea oublié d’implémenterarea. Sans elle, un résultat faux sortirait silencieusement.
import math
class Shape:
def area(self):
raise NotImplementedError("subclasses must implement area()")
class Square(Shape):
def __init__(self, side):
self.side = side
def area(self):
return self.side * self.side
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return round(math.pi * self.radius ** 2, 2)
for shape in [Square(3), Circle(3)]:
print(shape.area())9
28.27Résumé
- Le polymorphisme = le même appel, un comportement différent selon le type de l’objet. Le code appelant n’a pas besoin de connaître le type.
- Construisez une interface commune dans le parent (avec
raise NotImplementedErrorpour les méthodes obligatoires), et chaque enfant l’implémente à sa façon. - Il remplace les chaînes de
if isinstance/if type ==. Un nouveau type = une nouvelle classe, sans modifier le code existant. - Une méthode du parent qui appelle
self.x()atteint la version de l’enfant (Template Method). - Le Duck Typing : en Python, il suffit que l’objet ait la méthode. Un héritage commun reste recommandé comme documentation.
- Toutes les implémentations reçoivent les mêmes paramètres.