# L'héritage et super : des classes semblables sans code dupliqué<no value>

## Dans cette leçon

Cette leçon est construite comme une seule histoire, dans le même ordre que les diapositives :

1. **Chapitre 1** — pourquoi l'héritage : deux fois le même code, et comment hériter au lieu de copier
2. **Chapitre 2** — la redéfinition et `super()` : hériter, mais pas exactement — ne changer qu'une partie du comportement

À la fin de la leçon — de la pratique, et un **mini-projet** qui relie les deux chapitres.

## Chapitre 1 : Pourquoi l'héritage — parent, enfant et la relation « est une sorte de »

### Qu'allons-nous apprendre ?

- **Pourquoi** l'héritage est nécessaire : le problème des classes presque identiques.
- **La classe parente** (Parent Class, Base Class) et **la classe enfant** (Child Class, Subclass).
- **La syntaxe** de l'héritage en Python.
- **Ce qui se transmet** par l'héritage.
- **Le test is-a** : quand l'héritage convient et quand il ne convient pas.

### Pourquoi en avons-nous besoin ?

Dans la leçon sur les classes et les objets, nous nous sommes demandé : que se passe-t-il quand on ajoute un « compte d'épargne » semblable à un compte ordinaire, mais avec des intérêts ? Dans la version procédurale, chaque fonction se remplissait de `if account["type"] == ...`. Voyons le même problème en POO, **sans** héritage :

```python
class Employee:
    def __init__(self, name, salary):
        self.name = name
        self.salary = salary

    def monthly_cost(self):
        return self.salary * 1.25

    def describe(self):
        return self.name + " earns " + str(self.salary)

class Manager:
    def __init__(self, name, salary, team_size):
        self.name = name
        self.salary = salary
        self.team_size = team_size

    def monthly_cost(self):
        return self.salary * 1.25

    def describe(self):
        return self.name + " earns " + str(self.salary)

    def team_report(self):
        return self.name + " manages " + str(self.team_size) + " people"
```

`Manager` **copie** presque tout `Employee` : `name`, `salary`, `monthly_cost`, `describe`. Que se passe-t-il quand le coût employeur passe de 1.25 à 1.3 ? Il faut corriger **à deux endroits**, et s'il y a aussi `Intern`, `Contractor` et `Director`, à cinq. **C'est une violation de DRY au niveau des classes.**

Et autre chose : **un manager est un employé.** Tout ce qui est vrai pour un employé est aussi vrai pour un manager, et en plus le manager a d'autres choses. Le code doit refléter cette relation.

### Explication à partir des bases

#### L'héritage

**L'héritage** (Inheritance) = un mécanisme dans lequel une nouvelle classe **reçoit automatiquement** tous les attributs et toutes les méthodes d'une classe existante, et peut en **ajouter** ou en **modifier** une partie.

- **La classe parente** (Parent, Base, Superclass) — la classe générale. `Employee`.
- **La classe enfant** (Child, Derived, Subclass) — la classe particulière, qui **hérite**. `Manager`.

{{< box "analogy" "Analogie : un modèle de fabrication de voitures — la version sport" >}}
Nous avons un modèle de fabrication d'une « voiture » (le parent) : 4 roues, un moteur, une direction, des freins. Maintenant, nous voulons une « voiture de sport ». **Nous ne concevons pas une voiture à partir de zéro.** Nous prenons le modèle existant, et nous **ajoutons** un aileron et **changeons** le moteur. Chaque voiture de sport **est une voiture** (elle roule, elle freine), mais toute voiture n'est pas une voiture de sport.
{{< /box >}}

#### La relation « est une sorte de » (is-a)

L'héritage ne convient **que** lorsqu'on peut dire honnêtement : **« l'enfant est une sorte de parent »**.

| Phrase | Héritage ? |
|---|---|
| Un manager **est une sorte d'**employé | ✓ `Manager(Employee)` |
| Un chien **est une sorte d'**animal | ✓ `Dog(Animal)` |
| Un compte d'épargne **est une sorte de** compte bancaire | ✓ `SavingsAccount(BankAccount)` |
| Une voiture **est une sorte de** moteur ? | ✗ Une voiture **a** un moteur (has-a), et c'est de la **composition**, le thème de la conception orientée objet |
| Une commande **est une sorte de** client ? | ✗ Une commande **a** un client |

#### Qu'est-ce qui se transmet par l'héritage ?

L'enfant reçoit **tout** du parent :

- Le `__init__` (si l'enfant ne définit pas son propre `__init__`).
- **Toutes les méthodes**.
- **Les Class Attributes**.

Et l'enfant peut :

- **Ajouter** de nouvelles méthodes et de nouveaux attributs.
- **Modifier** (Override) des méthodes existantes. Nous l'apprendrons au chapitre suivant, avec `super()`.

#### La recherche par couches — encore

Quand vous écrivez `manager.describe()`, Python cherche : **(1)** sur l'objet, **(2)** dans sa classe (`Manager`), **(3)** dans le parent (`Employee`)... et s'arrête au premier endroit où il l'a trouvée. C'est la même idée de recherche par couches que nous avons vue avec LEGB et avec l'ordre de recherche des attributs.

### Syntaxe

```python
class Parent:
    def __init__(self, x):
        self.x = x

    def shared_method(self):
        return ...

class Child(Parent):             # Child inherits from Parent
    def extra_method(self):      # new method, only in Child
        return ...

c = Child(5)                     # uses Parent.__init__ (Child has none of its own)
c.shared_method()                # inherited
c.extra_method()                 # Child's own
```

- Le nom du parent vient **entre parenthèses** après le nom de l'enfant.
- Un enfant sans aucun ajout : `class Child(Parent): pass` est une classe valide identique au parent.

### Premier exemple de code

`week-3/employees.py` :

```python
class Employee:
    EMPLOYER_COST_RATE = 1.25

    def __init__(self, name, salary):
        self.name = name
        self.salary = salary

    def monthly_cost(self):
        return self.salary * Employee.EMPLOYER_COST_RATE

    def describe(self):
        return self.name + " earns " + str(self.salary)

class Manager(Employee):
    def team_report(self, team_size):
        return self.name + " manages " + str(team_size) + " people"

dana = Employee("Dana", 12000)
avi = Manager("Avi", 20000)

print(dana.describe())
print(avi.describe())
print(avi.monthly_cost())
print(avi.team_report(5))
```

**Sortie :**

```text
Dana earns 12000
Avi earns 20000
25000.0
Avi manages 5 people
```

### Analyse du code ligne par ligne

| Code | Explication |
|---|---|
| `class Manager(Employee):` | `Manager` **hérite** de `Employee`. |
| `Manager("Avi", 20000)` | `Manager` n'a pas son propre `__init__`, donc Python trouve celui d'`Employee` et l'utilise. Avi reçoit un `name` et un `salary`. |
| `avi.describe()` | Il n'y a pas de `describe` dans `Manager`. Python remonte à `Employee` et la trouve. **`self` est toujours Avi.** |
| `avi.monthly_cost()` | Pareil, et `EMPLOYER_COST_RATE` est aussi trouvé via le parent. |
| `avi.team_report(5)` | Une méthode qui n'existe **que** dans `Manager`. |
| `dana.team_report(...)` | Lancerait une `AttributeError` : un employé ordinaire n'hérite **pas** du manager. L'héritage va **de haut en bas** seulement. |

**Et la correction de 1.25 → 1.3 ?** On change **une seule ligne**, dans `Employee`, et tous les managers (et à l'avenir, tous les types d'employés) sont mis à jour.

### Un autre exemple

**Une hiérarchie avec plusieurs enfants : Animal / Dog / Cat.** `week-3/animals.py` :

```python
class Animal:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def birthday(self):
        self.age = self.age + 1

    def describe(self):
        return self.name + " (" + str(self.age) + " years old)"

class Dog(Animal):
    def fetch(self):
        return self.name + " brings back the ball"

class Cat(Animal):
    def climb(self):
        return self.name + " climbs the tree"

rex = Dog("Rex", 3)
mitzi = Cat("Mitzi", 5)

rex.birthday()
print(rex.describe(), "|", rex.fetch())
print(mitzi.describe(), "|", mitzi.climb())

for animal in [rex, mitzi]:
    print(type(animal).__name__, "->", animal.describe())
```

**Sortie :**

```text
Rex (4 years old) | Rex brings back the ball
Mitzi (5 years old) | Mitzi climbs the tree
Dog -> Rex (4 years old)
Cat -> Mitzi (5 years old)
```

**Analyse :**

- **Deux enfants du même parent** : `Dog` et `Cat` partagent tout ce qui est commun (`name`, `age`, `birthday`, `describe`), et chacun ajoute ce qui lui est propre.
- `rex.birthday()` — une méthode **écrite une seule fois** dans `Animal`, qui modifie l'état **de rex**.
- Dans la boucle, `animal.describe()` fonctionne sur **tout** type d'`Animal`, sans vérifier de quel type il s'agit. C'est un avant-goût du **polymorphisme**, que nous approfondirons dans la leçon suivante.
- `type(animal).__name__` — le nom de la classe sous forme de texte.

```text
         Animal
        /      \
      Dog      Cat
```

### Résumé

- **L'héritage** = une classe enfant reçoit tous les attributs et méthodes d'une classe parente, et peut en ajouter et en modifier.
- Syntaxe : `class Child(Parent):`.
- Il résout la **duplication** entre des classes semblables (DRY), et reflète la relation « **est une sorte de** » (is-a).
- Python cherche une méthode dans la classe de l'objet, et s'il ne l'a pas trouvée, remonte au parent.
- L'héritage est à sens unique : le parent ne sait pas ce qui a été ajouté dans l'enfant.
- **N'héritez pas** quand la relation est « **a un** » (has-a). On utilise alors la composition (le thème de la conception orientée objet).

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

### Documentation officielle

- [Python Tutorial — Inheritance](https://docs.python.org/3/tutorial/classes.html#inheritance)
- [isinstance()](https://docs.python.org/3/builtins/functions.html#isinstance)

<!-- docs:end -->

## Chapitre 2 : La redéfinition et `super()` — ne changer que ce qui est différent

### Qu'allons-nous apprendre ?

- **`super()`** : comment un enfant utilise le code du parent.
- **`__init__` dans un enfant** : ajouter des attributs sans copier ceux du parent.
- **La redéfinition de méthode** (Method Overriding) : un enfant **remplace** une méthode du parent.
- **L'extension** : un enfant **ajoute** à la méthode du parent, au lieu de la remplacer complètement.

### Pourquoi en avons-nous besoin ?

C'est là que l'héritage simple bloque : un `Truck` qui hérite d'un `Vehicle` a besoin d'un attribut supplémentaire (`cargo_kg`), c'est-à-dire de son propre `__init__`. Mais si nous écrivons un nouveau `__init__` dans Truck, il **remplace** celui de Vehicle, et alors `brand` et `wheels` ne seront pas initialisés, à moins de **copier** le code. Et c'est exactement ce que l'héritage devait éviter.

Et un deuxième problème : parfois l'enfant doit se comporter **différemment** pour la même action. Un manager reçoit une prime dans le calcul du coût. Un compte d'épargne ne permet pas de retirer en dessous d'un minimum. Il nous faut un moyen de **modifier** une méthode reçue par héritage.

### Explication à partir des bases

#### La redéfinition de méthode (Method Overriding)

Si dans l'enfant vous définissez une méthode **du même nom** que dans le parent, **la version de l'enfant l'emporte** pour les objets de type enfant. C'est ce qu'on appelle une **redéfinition** (Override). Pourquoi ? Souvenez-vous de l'ordre de recherche : Python cherche **d'abord** dans la classe de l'objet, et seulement s'il n'a pas trouvé, remonte au parent. Si la méthode existe dans l'enfant, Python n'atteint même jamais la version du parent.

#### `super()` — « la version du parent »

**`super()`** renvoie un objet spécial à travers lequel on appelle **la méthode du parent**, sur **le même `self`**. Les deux usages principaux :

1. **Dans `__init__`** : `super().__init__(...)` — « initialise d'abord tout ce que le parent initialise », puis ajoute ce qui est **propre** à l'enfant.
2. **Pour étendre une méthode** : `super().describe()` — « donne-moi ce que renvoie le parent », puis ajoute-y quelque chose.

{{< box "analogy" "Analogie : une recette de famille" >}}
Grand-mère (le parent) fait un gâteau au chocolat selon une recette. Vous (l'enfant) **héritez** de la recette. **Redéfinir** = vous décidez de faire un gâteau complètement différent sous le même nom, « gâteau au chocolat », mais végan, à partir de zéro. **Étendre avec `super()`** = « fais exactement comme grand-mère » (`super().bake()`), **puis** ajoute un glaçage que vous avez inventé. Vous ne recopiez pas toute la recette de grand-mère. Vous dites simplement « comme grand-mère, plus... ».
{{< /box >}}

#### Trois options pour une méthode dans un enfant

| Option | Ce que vous faites | Quand |
|---|---|---|
| **Hériter** | Vous n'écrivez rien | Le comportement du parent convient |
| **Redéfinir** | Vous écrivez une méthode du même nom, sans `super()` | Le comportement est complètement différent |
| **Étendre** | Vous écrivez une méthode du même nom, et à l'intérieur `super().method()` | « Comme le parent, plus quelque chose » |

### Syntaxe

```python
class Parent:
    def __init__(self, a):
        self.a = a

    def describe(self):
        return "a=" + str(self.a)

    def greet(self):
        return "Hello from Parent"

class Child(Parent):
    def __init__(self, a, b):
        super().__init__(a)          # let Parent set up self.a
        self.b = b                   # then add what is new in Child

    def describe(self):              # EXTEND: reuse Parent's result
        return super().describe() + ", b=" + str(self.b)

    def greet(self):                 # OVERRIDE: replace completely
        return "Hello from Child"
```

**Remarquez :** dans `super().__init__(a)`, on ne passe **pas** `self`. `super()` « sait » déjà de quel objet il s'agit.

### Premier exemple de code

**Résoudre le problème de Truck.** `week-3/vehicles_super.py` :

```python
class Vehicle:
    def __init__(self, brand, wheels):
        self.brand = brand
        self.wheels = wheels

    def describe(self):
        return self.brand + " with " + str(self.wheels) + " wheels"

class Truck(Vehicle):
    def __init__(self, brand, max_load_kg):
        super().__init__(brand, 6)
        self.max_load_kg = max_load_kg
        self.cargo_kg = 0

    def load(self, kg):
        if self.cargo_kg + kg > self.max_load_kg:
            raise ValueError("over the limit: max " + str(self.max_load_kg) + " kg")
        self.cargo_kg = self.cargo_kg + kg

    def describe(self):
        return super().describe() + ", carrying " + str(self.cargo_kg) + "/" + str(self.max_load_kg) + " kg"

volvo = Truck("Volvo", 1000)
volvo.load(600)
print(volvo.describe())

try:
    volvo.load(500)
except ValueError as error:
    print("Rejected:", error)

print(volvo.brand, volvo.wheels)
```

**Sortie :**

```text
Volvo with 6 wheels, carrying 600/1000 kg
Rejected: over the limit: max 1000 kg
Volvo 6
```

### Analyse du code ligne par ligne

| Code | Explication |
|---|---|
| `def __init__(self, brand, max_load_kg):` | Truck a **son propre** `__init__`, avec des paramètres adaptés à un camion. |
| `super().__init__(brand, 6)` | « Exécute le `__init__` de Vehicle **sur cet objet** ». Cela crée `self.brand` et `self.wheels`. Un camion a toujours 6 roues, c'est donc l'enfant qui décide de la valeur. |
| `self.max_load_kg = ...`, `self.cargo_kg = 0` | **Seulement** ce qui est nouveau dans Truck. Sans copier une seule ligne de Vehicle. |
| `def describe(self):` dans Truck | **L'extension** : le même nom que dans Vehicle. |
| `super().describe()` | Reçoit `"Volvo with 6 wheels"` du parent, et y ajoute les informations de chargement. |
| `volvo.brand`, `volvo.wheels` | Ils existent, car `super().__init__` les a créés. |

**Si Vehicle change demain**, par exemple ajoute `self.year`, les camions le recevront automatiquement via `super().__init__`. Si nous avions copié le code, il faudrait penser à mettre à jour Truck aussi.

### Un autre exemple

**Une redéfinition complète, une extension, et `super()` dans des règles métier.** `week-3/staff.py` :

```python
class Employee:
    EMPLOYER_COST_RATE = 1.25

    def __init__(self, name, salary):
        self.name = name
        self.salary = salary

    def monthly_cost(self):
        return self.salary * Employee.EMPLOYER_COST_RATE

    def title(self):
        return "Employee"

    def describe(self):
        return self.title() + " " + self.name + " - cost " + str(self.monthly_cost())

class Manager(Employee):
    def __init__(self, name, salary, bonus):
        super().__init__(name, salary)
        self.bonus = bonus

    def monthly_cost(self):
        return super().monthly_cost() + self.bonus

    def title(self):
        return "Manager"

class Intern(Employee):
    def monthly_cost(self):
        return self.salary

    def title(self):
        return "Intern"

staff = [Employee("Dana", 12000), Manager("Avi", 20000, 3000), Intern("Noa", 5000)]

for person in staff:
    print(person.describe())

print("Total monthly cost:", sum(person.monthly_cost() for person in staff))
```

**Sortie :**

```text
Employee Dana - cost 15000.0
Manager Avi - cost 28000.0
Intern Noa - cost 5000
Total monthly cost: 48000.0
```

**Analyse :**

- `Manager.monthly_cost` — **l'extension** : « comme un employé ordinaire (`super().monthly_cost()` = 25 000), **plus** une prime ». Si le taux du coût employeur change, les managers recevront la modification automatiquement.
- `Intern.monthly_cost` — **une redéfinition complète** : un stagiaire n'entraîne pas de coûts employeur, donc un calcul complètement différent, sans `super()`.
- `title()` — chaque classe la redéfinit.
- **Le point intéressant :** `describe()` n'est écrite **qu'une seule fois**, dans `Employee`. Elle appelle `self.title()` et `self.monthly_cost()`. Quand l'objet est un Manager, `self.monthly_cost()` atteint **la version du Manager** ! La méthode du parent utilise automatiquement les versions redéfinies de l'enfant. C'est la base du **polymorphisme** (la leçon suivante).
- `Intern` n'a pas défini de `__init__`, il utilise donc celui d'`Employee` (l'héritage ordinaire).

### Résumé

- **Redéfinir** (Override) = une méthode dans l'enfant du même nom que dans le parent. La version de l'enfant l'emporte.
- **`super()`** = l'accès à la version du parent, sur le même `self`.
- Un enfant avec son propre `__init__` **doit** appeler `super().__init__(...)` (sans `self`), puis n'ajouter que ce qui est nouveau.
- **Étendre** : `super().method()` + un ajout. **Une redéfinition complète** : sans `super()`.
- `self.method()` dans une méthode redéfinie = une récursion. Pour la version du parent : `super().method()`.
- Une méthode du parent qui appelle `self.x()` atteindra automatiquement la version redéfinie de l'enfant.
- Une méthode redéfinie garde **la même signature** (les mêmes paramètres).

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

### Documentation officielle

- [super()](https://docs.python.org/3/builtins/functions.html#super)
- [Python Tutorial — Inheritance](https://docs.python.org/3/tutorial/classes.html#inheritance)

<!-- docs:end -->

<!-- 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 :** l'héritage — parent et enfant, le test « est une sorte de », étendre `__init__` avec `super()`, et redéfinir une méthode (Override).

**À la fin de la page, vous saurez :**
- Écrire un enfant qui hérite d'un parent et n'ajoute que ce qui est nouveau
- Décider quand l'héritage convient, grâce au test « est une sorte de »
- Appeler `super().__init__` pour que le parent initialise sa partie
- Redéfinir une méthode, et utiliser la version du parent à l'intérieur

### Exercice 1: Un employé et un manager

**Ce que vous construisez :** un employé et un manager — où un manager est une sorte d'employé, pour ne pas réécrire ce qui est commun.

**Ce qu'il faut écrire :**
1. Une classe nommée `Employee` avec un nom et un salaire.
2. Dans `Employee` : `describe` qui renvoie le nom et le salaire, comme dans la sortie.
3. Une classe nommée `Manager` qui hérite d'`Employee` — sans son propre `__init__`.
4. Dans `Manager` uniquement : `manage` qui reçoit la taille d'une équipe et renvoie une phrase comme dans la sortie.

**Code de test :**

```python
dana = Employee("Dana", 12000)
avi = Manager("Avi", 20000)
print(dana.describe())
print(avi.describe())
print(avi.manage(5))
```

**La sortie souhaitée :**

```text
Dana: 12000
Avi: 20000
Avi manages 5 people
```

{{< solution >}}
```python
class Employee:
    def __init__(self, name, salary):
        self.name = name
        self.salary = salary

    def describe(self):
        return self.name + ": " + str(self.salary)


class Manager(Employee):
    def manage(self, team_size):
        return self.name + " manages " + str(team_size) + " people"


dana = Employee("Dana", 12000)
avi = Manager("Avi", 20000)
print(dana.describe())
print(avi.describe())
print(avi.manage(5))
```

```text
Dana: 12000
Avi: 20000
Avi manages 5 people
```

`Manager` **hérite** de `__init__` et de `describe` — sans les réécrire — et n'ajoute que ce qui est nouveau.
{{< /solution >}}

### Exercice 2: is-a — est une sorte de

**Ce que vous faites :** décider — sans code — quand l'héritage convient.

**La tâche :** pour chaque paire, répondez : le premier doit-il hériter du second ? Expliquez avec la phrase « X est une sorte de Y » — et si la phrase n'est pas vraie, quelle est en réalité la relation entre eux.

1. `Dog` et `Animal`
2. `Car` et `Engine`
3. `SavingsAccount` et `BankAccount`
4. `Square` et `Color`

**Ce qu'il faut rendre :** oui ou non pour chaque paire, et une phrase d'explication.

{{< solution >}}
1. **Oui** — un chien **est une sorte d'**animal.
2. **Non** — une voiture n'est **pas** une sorte de moteur. Une voiture **a** un moteur (has-a). C'est un cas de composition, pas d'héritage.
3. **Oui** — un compte d'épargne **est une sorte de** compte bancaire.
4. **Non** — un carré n'est pas une sorte de couleur. Un carré **a** peut-être une couleur.

L'héritage seulement quand la phrase « X est une sorte de Y » est vraie.
{{< /solution >}}

### Exercice 3: Un arbre d'animaux

**Ce que vous construisez :** un petit arbre d'héritage — un animal, et deux sortes d'animaux, chacune avec une action propre.

**Ce qu'il faut écrire :**
1. Une classe nommée `Animal` avec un nom et un âge, et `describe` qui renvoie le nom et l'âge comme dans la sortie.
2. Une classe nommée `Dog` qui hérite d'`Animal`, avec `fetch` qui renvoie la phrase de la sortie.
3. Une classe nommée `Cat` qui hérite d'`Animal`, avec `climb` qui renvoie la phrase de la sortie.

**Code de test :**

```python
rex = Dog("Rex", 4)
mitzi = Cat("Mitzi", 5)
print(rex.describe(), "|", rex.fetch())
print(mitzi.describe(), "|", mitzi.climb())
```

**La sortie souhaitée :**

```text
Rex (4 years old) | Rex brings back the ball
Mitzi (5 years old) | Mitzi climbs the tree
```

{{< solution >}}
```python
class Animal:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def describe(self):
        return self.name + " (" + str(self.age) + " years old)"


class Dog(Animal):
    def fetch(self):
        return self.name + " brings back the ball"


class Cat(Animal):
    def climb(self):
        return self.name + " climbs the tree"


rex = Dog("Rex", 4)
mitzi = Cat("Mitzi", 5)
print(rex.describe(), "|", rex.fetch())
print(mitzi.describe(), "|", mitzi.climb())
```

```text
Rex (4 years old) | Rex brings back the ball
Mitzi (5 years old) | Mitzi climbs the tree
```
{{< /solution >}}

### Exercice 4: super().__init__

**Ce que vous construisez :** un camion — un véhicule qui a toujours 6 roues, et qui a aussi un poids de chargement maximum.

**Ce qu'il faut écrire :**
1. Une classe nommée `Vehicle` avec une marque et un nombre de roues.
2. Une classe nommée `Truck` qui hérite de `Vehicle`. Son `__init__` reçoit une marque et un poids maximum.
3. Dans le `__init__` de `Truck` : appelez `super().__init__` avec la marque et 6 roues, et initialisez un poids actuel de 0.
4. `load` — reçoit des kilogrammes et les ajoute au poids. Si le maximum est dépassé : une `ValueError` avec le message de la sortie.

**Code de test :**

```python
truck = Truck("Volvo", 1000)
truck.load(600)
print(truck.brand, truck.wheels, truck.load_kg)
try:
    truck.load(500)
except ValueError as error:
    print("error:", error)
```

**La sortie souhaitée :**

```text
Volvo 6 600
error: overloaded: max 1000 kg
```

{{< solution >}}
```python
class Vehicle:
    def __init__(self, brand, wheels):
        self.brand = brand
        self.wheels = wheels


class Truck(Vehicle):
    def __init__(self, brand, max_load_kg):
        super().__init__(brand, 6)
        self.max_load_kg = max_load_kg
        self.load_kg = 0

    def load(self, kg):
        if self.load_kg + kg > self.max_load_kg:
            raise ValueError("overloaded: max " + str(self.max_load_kg) + " kg")
        self.load_kg = self.load_kg + kg


truck = Truck("Volvo", 1000)
truck.load(600)
print(truck.brand, truck.wheels, truck.load_kg)
try:
    truck.load(500)
except ValueError as error:
    print("error:", error)
```

```text
Volvo 6 600
error: overloaded: max 1000 kg
```

`super().__init__(brand, 6)` initialise la partie du parent. Ensuite, l'enfant ajoute ses propres attributs.
{{< /solution >}}

### Exercice 5: Redéfinir avec super

**Ce que vous construisez :** un manager dont le coût mensuel est le salaire plus une prime.

**Ce qu'il faut écrire :**
1. Une classe nommée `Employee` avec un nom et un salaire, et `monthly_cost` qui renvoie le salaire.
2. Une classe nommée `Manager` qui hérite d'`Employee`, et reçoit aussi une prime. Dans son `__init__`, appelez `super().__init__`.
3. Dans `Manager`, redéfinissez `monthly_cost` : la version du parent, via `super()`, plus la prime.

**Code de test :**

```python
print(Employee("Dana", 12000).monthly_cost())
print(Manager("Avi", 20000, 3000).monthly_cost())
```

**La sortie souhaitée :**

```text
12000
23000
```

{{< solution >}}
```python
class Employee:
    def __init__(self, name, salary):
        self.name = name
        self.salary = salary

    def monthly_cost(self):
        return self.salary


class Manager(Employee):
    def __init__(self, name, salary, bonus):
        super().__init__(name, salary)
        self.bonus = bonus

    def monthly_cost(self):
        return super().monthly_cost() + self.bonus


print(Employee("Dana", 12000).monthly_cost())
print(Manager("Avi", 20000, 3000).monthly_cost())
```

```text
12000
23000
```

`super().monthly_cost()` appelle la version du parent. Si le calcul du salaire de base change, `Manager` recevra la modification automatiquement.
{{< /solution >}}

## Mini-projet: Les employés d'une entreprise

**Quel système vous construisez :** un système d'employés pour une entreprise. L'entreprise a des employés ordinaires, des managers et des stagiaires. Ce sont tous des employés — ils ont un nom et un salaire — mais chaque type s'affiche différemment, et un manager a en plus une prime. Le système affiche chaque employé et calcule le coût mensuel de tous ensemble.

**Ce que vous apprendrez du projet :**
- Construire un parent avec tout ce qui est commun, et des enfants qui n'ajoutent que ce qui est différent
- Appeler `super().__init__` depuis l'enfant
- Redéfinir une méthode, et étendre une méthode avec `super()`
- Voir comment une méthode écrite une seule fois dans le parent fonctionne correctement pour chaque type

**Ce qu'il faut construire — étape par étape :**

**Étape 1 — un employé (`Employee`).** `__init__` reçoit un nom et un salaire. Trois méthodes :
- `title` — renvoie le mot Employee ;
- `monthly_cost` — renvoie le salaire ;
- `describe` — renvoie le nom, le rôle et le coût mensuel, comme dans la sortie. Elle utilise `title` et `monthly_cost`.

**Étape 2 — un manager (`Manager`).** Hérite d'`Employee` et reçoit aussi une prime. Dans `__init__`, appelez `super().__init__` et enregistrez la prime. Redéfinissez `title` — le mot Manager. Étendez `monthly_cost` : le résultat de `super()` plus la prime.

**Étape 3 — un stagiaire (`Intern`).** Hérite d'`Employee` et reçoit aussi un nombre de mois de stage. Dans `__init__`, vérifiez que le nombre est entre 1 et 12 — sinon `ValueError` — puis appelez `super().__init__`. Redéfinissez `title` — le mot Intern. N'écrivez **pas** `monthly_cost` ni `describe` : ils viennent par héritage.

**Le message d'erreur — exactement ainsi :**

```text
months must be 1-12
```

**Code de test :**

```python
staff = [
    Employee("Dana", 12000),
    Manager("Avi", 20000, 3000),
    Intern("Noa", 6000, 6),
]

for person in staff:
    print(person.describe())

try:
    Intern("Tom", 5000, 0)
except ValueError as error:
    print("error:", error)

total = 0
for person in staff:
    total = total + person.monthly_cost()
print("Total monthly cost:", total)
```

**La sortie souhaitée :**

```text
Dana (Employee): 12000
Avi (Manager): 23000
Noa (Intern): 6000
error: months must be 1-12
Total monthly cost: 41000
```

**À la fin du projet, vous saurez :**
- Concevoir une hiérarchie : ce qui monte dans le parent et ce qui reste dans l'enfant
- Distinguer trois options pour une méthode dans un enfant : hériter, redéfinir, ou étendre avec `super()`
- Comprendre pourquoi `describe` montre à chaque employé **son propre** rôle et **son propre** coût, alors qu'elle n'a été écrite qu'une fois

**Bonus :** ajoutez à `Manager` une méthode `manage` qui reçoit la taille d'une équipe et renvoie une phrase comme Avi manages 5 people, et affichez-la uniquement pour les managers de la liste.

{{< solution >}}
```python
class Employee:
    def __init__(self, name, salary):
        self.name = name
        self.salary = salary

    def title(self):
        return "Employee"

    def monthly_cost(self):
        return self.salary

    def describe(self):
        return self.name + " (" + self.title() + "): " + str(self.monthly_cost())


class Manager(Employee):
    def __init__(self, name, salary, bonus):
        super().__init__(name, salary)
        self.bonus = bonus

    def title(self):
        return "Manager"

    def monthly_cost(self):
        return super().monthly_cost() + self.bonus


class Intern(Employee):
    def __init__(self, name, salary, months):
        if months < 1 or months > 12:
            raise ValueError("months must be 1-12")
        super().__init__(name, salary)
        self.months = months

    def title(self):
        return "Intern"


staff = [
    Employee("Dana", 12000),
    Manager("Avi", 20000, 3000),
    Intern("Noa", 6000, 6),
]

for person in staff:
    print(person.describe())

try:
    Intern("Tom", 5000, 0)
except ValueError as error:
    print("error:", error)

total = 0
for person in staff:
    total = total + person.monthly_cost()
print("Total monthly cost:", total)
```

```text
Dana (Employee): 12000
Avi (Manager): 23000
Noa (Intern): 6000
error: months must be 1-12
Total monthly cost: 41000
```

- `describe` a été écrite **une seule fois** dans `Employee`, mais chaque employé reçoit **son propre** `title()` et **son propre** `monthly_cost()` — car Python cherche la méthode d'abord dans l'enfant.
- `Manager.monthly_cost` **étend** : `super().monthly_cost()` renvoie le salaire, et la prime s'y ajoute. `title` **redéfinit** complètement.
- `Intern` n'écrit pas `monthly_cost` — il en **hérite** telle quelle.

**Bonus :**

```python
    def manage(self, team_size):
        return self.name + " manages " + str(team_size) + " people"
```

(Une méthode dans `Manager`.)

```python
for person in staff:
    if person.title() == "Manager":
        print(person.manage(5))
```

```text
Avi manages 5 people
```
{{< /solution >}}

<!-- exercises:end -->
