L'héritage et super : des classes semblables sans code dupliqué

Objectifs de la leçon
  • Expliquer quand l’héritage évite du code dupliqué, et le vérifier avec le test « est une sorte de »
  • Définir une classe parente et une classe enfant, et comprendre comment Python cherche une méthode — d’abord dans l’enfant puis dans le parent
  • Redéfinir une méthode dans l’enfant pour changer son comportement
  • Utiliser super() — à la fois dans __init__ et pour étendre une méthode du parent

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 :

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

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 ».

PhraseHé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

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 :

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 :

Dana earns 12000
Avi earns 20000
25000.0
Avi manages 5 people

Analyse du code ligne par ligne

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

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 :

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.
         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).

Documentation officielle

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.
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… ».

Trois options pour une méthode dans un enfant

OptionCe que vous faitesQuand
HériterVous n’écrivez rienLe comportement du parent convient
RedéfinirVous écrivez une méthode du même nom, sans super()Le comportement est complètement différent
ÉtendreVous écrivez une méthode du même nom, et à l’intérieur super().method()« Comme le parent, plus quelque chose »

Syntaxe

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 :

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 :

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

Analyse du code ligne par ligne

CodeExplication
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 = 0Seulement ce qui est nouveau dans Truck. Sans copier une seule ligne de Vehicle.
def describe(self): dans TruckL’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.wheelsIls 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 :

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 :

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).

Documentation officielle

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 :

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

La sortie souhaitée :

Dana: 12000
Avi: 20000
Avi manages 5 people
Afficher la solution
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))
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.

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.

Afficher la 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.

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 :

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

La sortie souhaitée :

Rex (4 years old) | Rex brings back the ball
Mitzi (5 years old) | Mitzi climbs the tree
Afficher la solution
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())
Rex (4 years old) | Rex brings back the ball
Mitzi (5 years old) | Mitzi climbs the tree

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 :

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 :

Volvo 6 600
error: overloaded: max 1000 kg
Afficher la solution
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)
Volvo 6 600
error: overloaded: max 1000 kg

super().__init__(brand, 6) initialise la partie du parent. Ensuite, l’enfant ajoute ses propres attributs.

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 :

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

La sortie souhaitée :

12000
23000
Afficher la solution
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())
12000
23000

super().monthly_cost() appelle la version du parent. Si le calcul du salaire de base change, Manager recevra la modification automatiquement.

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 :

months must be 1-12

Code de test :

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 :

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.

Afficher la solution
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)
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 :

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

(Une méthode dans Manager.)

for person in staff:
    if person.title() == "Manager":
        print(person.manage(5))
Avi manages 5 people