# Le polymorphisme : une interface commune, des comportements différents<no value>

## 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 :

```python
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.

{{< box "analogy" "Analogie : une télécommande universelle" >}}
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.
{{< /box >}}

### Comment ça marche : une interface commune

1. Dans le **parent**, définissez **l'interface** : quelles méthodes ont tous les types (`pay`, `refund`, `fee`).
2. Chaque **enfant** **redéfinit** les méthodes à sa façon.
3. 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

```python
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/elif
```

**`raise 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` :

```python
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 :**

```text
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: 0
```

## Analyse 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.

```python
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)
```

```text
Paid 101 with Apple Pay on iPhone 15 | fee: 1
```

**`checkout` 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 :**

```python
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)
```

```text
Redeemed 100 from gift card GIFT-2026 | fee: 0
```

`GiftCard` 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

```python
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")
```

```text
Email to dana@mail.com: Your order #1042 has shipped and will arrive tomorrow
SMS to 050-1234567: Your order #1042 has
```

Chaque 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.**

```python
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.**

```python
class Cash(PaymentMethod):
    pass

checkout(Cash("Rina"), 50)
```

```text
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.**

```python
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.

<!-- exercises:start — generated by scripts/intro6/sync_site_exercises.py; edit scripts/intro6/exercises/ -->
## 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 `if` selon 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 :**

```text
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 :**
1. Une classe de base nommée `PaymentMethod`, avec `pay` qui reçoit un montant et lance `NotImplementedError`.
2. Deux enfants : `CreditCard` et `PayPal`. Chacun redéfinit `pay` et renvoie sa propre phrase de la sortie.
3. Une fonction nommée `checkout` qui reçoit un moyen de paiement et un montant, et appelle `pay` — sans `if` sur le type.

**Code de test :**

```python
for method in [CreditCard(), PayPal()]:
    print(checkout(method, 100))
```

**La sortie souhaitée :**

```text
Charging card 100
Redirecting to PayPal 100
```

{{< solution >}}
```python
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))
```

```text
Charging card 100
Redirecting to PayPal 100
```

`checkout` 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**.
{{< /solution >}}

### 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 :**
1. Un nouvel enfant nommé `ApplePay`, avec `pay` qui renvoie sa propre phrase de la sortie.
2. Dans `PaymentMethod`, ajoutez `fee` qui reçoit un montant et renvoie 0 — une valeur par défaut pour tous.
3. Dans `CreditCard`, redéfinissez `fee` : des frais de 2 % du montant.
4. Mettez à jour `checkout` pour qu'il renvoie aussi les frais, comme dans la sortie.

**Code de test :**

```python
for method in [CreditCard(), PayPal(), ApplePay()]:
    print(checkout(method, 100))
```

**La sortie souhaitée :**

```text
Charging card 100 | fee: 2.0
Redirecting to PayPal 100 | fee: 0
Paid with Apple Pay 100 | fee: 0
```

{{< solution >}}
```python
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))
```

```text
Charging card 100 | fee: 2.0
Redirecting to PayPal 100 | fee: 0
Paid with Apple Pay 100 | fee: 0
```

`ApplePay` a été ajouté sans changer une seule ligne de `checkout`. `PayPal` et `ApplePay` héritent du `fee` du parent, et `CreditCard` le redéfinit.
{{< /solution >}}

### 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 :**
1. Une classe de base nommée `Character`, avec un nom et une santé, et `attack_power` qui lance `NotImplementedError`.
2. `Warrior` — sa puissance d'attaque vaut toujours 15.
3. `Mage` — sa puissance d'attaque vaut 10, plus 10 s'il a plus de 50 de santé.
4. Une fonction nommée `attack` qui 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 :**

```python
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 :**

```text
Conan hits Merlin for 15
Merlin hits Conan for 10
Merlin 45
Conan 90
```

{{< solution >}}
```python
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)
```

```text
Conan hits Merlin for 15
Merlin hits Conan for 10
Merlin 45
Conan 90
```

Aprè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.
{{< /solution >}}

### Exercice 4: Le Duck Typing

**Ce que vous faites :** lire du code et expliquer — sans écrire de code.

**Le code :**

```text
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 :**
1. `Duck` et `Robot` n'héritent pas du même parent. `make_it_speak` fonctionnera-t-elle avec les deux ? Pourquoi ?
2. Expliquez en une phrase ce qu'est le Duck Typing.

{{< solution >}}
1. **Oui.** `make_it_speak` appelle seulement `speak()` — elle ne vérifie pas le type. Tout objet qui a `speak` convient.
2. **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.
{{< /solution >}}

### Exercice 5: NotImplementedError

**Ce que vous construisez :** des formes qui savent chacune calculer leur propre aire — et un parent qui l'exige.

**Le code :**

```text
class Shape:
    def area(self):
        raise NotImplementedError("subclasses must implement area()")

class Square(Shape):
    def __init__(self, side):
        self.side = side
```

**La tâche :**
1. Que se passera-t-il quand vous appellerez l'aire de `Square(3)` ? Pourquoi est-ce en fait une bonne chose ?
2. Corrigez `Square` pour qu'il calcule une aire.
3. Ajoutez `Circle` qui reçoit un rayon, et calcule l'aire avec `math.pi`, arrondie à deux chiffres.
4. Affichez dans une boucle l'aire d'un carré de côté 3 et d'un cercle de rayon 3.

**La sortie souhaitée :**

```text
9
28.27
```

{{< solution >}}
1. 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 écrit `Square` a oublié d'implémenter `area`. Sans elle, un résultat faux sortirait silencieusement.
2.

```python
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())
```

```text
9
28.27
```
{{< /solution >}}

<!-- exercises:end -->

## Ré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 NotImplementedError` pour 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**.

<!-- docs:start — generated by scripts/intro6/site_docs.py; edit the links there -->

## Documentation officielle

- [Python Glossary — duck-typing](https://docs.python.org/3/glossary.html#term-duck-typing)
- [Python Tutorial — Inheritance](https://docs.python.org/3/tutorial/classes.html#inheritance)

<!-- docs:end -->
