Qu'est-ce que l'héritage en Python ?

L'héritage est un élément essentiel en programmation orientée objet (POO). Il permet à une classe enfant (sous-classe) de recevoir les attributs et les méthodes d'une classe parente. Un mécanisme qui favorise la réutilisation du code tout en mettant en place une hiérarchie logique entre les classes.

Ce qui veut dire qu'une classe enfant peut hériter des fonctionnalités de la classe parente, les étendre ou les modifier selon nos besoins.

Classe Parente et Classe Enfant

  • La classe parente est la classe dont les fonctionnalités sont héritées

  • La classe enfant est la classe qui hérite

La syntaxe pour utiliser l'héritage est simple : il suffit d'indiquer le nom de la classe parente entre parenthèses après le nom de la classe enfant.

# Classe Parente
class Animal:
    def __init__(self, nom):
        self.nom = nom

    def manger(self):
        print(f"{self.nom} est en train de manger.")

# Classe Enfant qui hérite de Animal
class Chien(Animal):
    def aboyer(self):
        print(f"{self.nom} fait 'Woof!'.")

# Utilisation
mon_chien = Chien("Rex")
mon_chien.manger()  # Méthode héritée de la classe Animal
mon_chien.aboyer()  # Méthode propre à la classe Chien
# Rex est en train de manger.
# Rex fait 'Woof!'.
Un instant

Dans l'exemple ci-dessus, la classe Chien hérite de la méthode manger() de la classe Animal, tout en implémentant son propre comportement aboyer().

Héritage des attributs

Attributs d'instance

Une classe enfant hérite des attributs d'instance et peut en ajouter de nouveaux. Dans ce cas, on appelle le constructeur de classe parente avec la fonction super().

class Animal:
    def __init__(self, nom): # méthode spéciale (__init__)
        self.nom = nom

    def manger(self):
        print(f"{self.nom} est en train de manger.")


class Chien(Animal):
    def __init__(self, nom, race):
        # Initialise l'attribut d'instance 'nom' de la classe parente
        super().__init__(nom)
        # Ajoute un nouvel attribut d'instance spécifique à la classe Chien
        self.race = race

mon_chien_de_race = Chien("Médor", "Golden Retriever")
print(f"Nom : {mon_chien_de_race.nom}") # Attribut hérité
print(f"Race : {mon_chien_de_race.race}") # Attribut propre
# Nom : Médor
# Race : Golden Retriever
Un instant

Attribut de classe

Un attribut de classe est défini directement dans le scope de la classe. Une classe enfant hérite des attributs de classe, mais peut aussi les modifier ou en ajouter de nouveaux.

class Vehicule:
    # Attribut de classe parent
    roues = 4
    moteur = True

class Moto(Vehicule):
    # Surcharge d'un attribut de classe hérité
    roues = 2

class Velo(Vehicule):
    # Surcharge et ajout d'attributs de classe
    roues = 2
    moteur = False
    pedales = True

voiture = Vehicule()
print(f"Roues voiture : {voiture.roues}, Moteur voiture : {voiture.moteur}")

moto = Moto()
print(f"Roues moto : {moto.roues}, Moteur moto : {moto.moteur}") # 'moteur' est hérité

velo = Velo()
print(f"Roues vélo : {velo.roues}, Moteur vélo : {velo.moteur}, Pédales vélo : {velo.pedales}")

# Roues voiture : 4, Moteur voiture : True
# Roues moto : 2, Moteur moto : True
# Roues vélo : 2, Moteur vélo : False, Pédales vélo : True
Un instant

La classe Moto hérite de Vehicule et surcharge l'attribut roues. La classe Velo ajoute l'attribut pedales, surcharge les attributs roues et moteur.

Surcharge de méthode

Nous avons vu précédemment que l'on pouvait surcharger la méthode __init__ pour ajouter de nouveaux attributs d'instance.

Une classe enfant peut fournir une implémentation spécifique d'une méthode déjà définie dans la classe parente : c'est ce que l'on appelle la surcharge de méthode.

class Animal:
    def __init__(self, nom):
        self.nom = nom

    def manger(self):
        print(f"{self.nom} est en train de manger.")


class Oiseau(Animal):
    def manger(self):
        print(f"{self.nom} picore des graines.")

mon_oiseau = Oiseau("Titi")
mon_oiseau.manger() # Appelle la méthode surchargée de Oiseau, pas celle de Animal

# Résultat
# Titi picore des graines.
Un instant

La classe Oiseau redéfinit la méthode manger() pour qu'elle corresponde à son comportement spécifique.

Étendre une méthode avec super()

On peut remplacer le comportement de la méthode de la classe parente, mais on peut aussi étendre son comportement.

class Animal:
    def __init__(self, nom):
        self.nom = nom

    def se_presenter(self):
        print(f"Je suis un animal du nom de {self.nom}.")

class Chat(Animal):
    def __init__(self, nom, couleur):
        super().__init__(nom)
        self.couleur = couleur

    def se_presenter(self):
        # Appelle d'abord la méthode originale de la classe parente
        super().se_presenter()
        # Ajoute ensuite le comportement spécifique à la classe enfant
        print(f"Je suis un félin de couleur {self.couleur}.")

mon_chat = Chat("Félix", "noir")
mon_chat.se_presenter()

# Résultat
# Je suis un animal du nom de Félix.
# Je suis un félin de couleur noir.
Un instant

mon_chat.se_presenter() va afficher le message de la classe parente via super et le message qui a été ajouté dans la classe Chat.

Héritage multiple

Il est possible de combiner la fonctionnalités de plusieurs classes en héritant de plusieurs classes parentes.

class Animal:
    def __init__(self, nom):
        self.nom = nom

    def manger(self):
        print(f"{self.nom} est en train de manger.")


class Predateur:
    def chasser(self):
        print("Je chasse.")

class Proie:
    def fuir(self):
        print("Je fuis.")

# Le Lion hérite à la fois de Animal et de Predateur
class Lion(Animal, Predateur):
    pass

# Le Lapin hérite à la fois de Animal et de Proie
class Lapin(Animal, Proie):
    pass

simba = Lion("Simba")
simba.manger()  # De Animal
simba.chasser() # De Predateur

panpan = Lapin("Panpan")
panpan.manger() # De Animal
panpan.fuir()   # De Proie


# Résultat
# Simba est en train de manger.
# Je chasse.
# Panpan est en train de manger.
# Je fuis.
Un instant

La classe Lion hérite de Animal et de Predateur, ce qui permet d'utiliser les méthodes des deux classes parentes.

L'ordre dans lequel les classes parentes est important. On parlera de l'ordre de résolution des méthodes (MRO). De manière générale l'ordre est de gauche à droite.

class A:
    ...


class B:
    ...


class C(A, B):
    ...


print(C.mro())
# [<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>, <class 'object'>]

La méthode mro() permet d'afficher l'ordre de résolution. On remarque bien que l'ordre de gauche à droite est bien respectée : C, A, B et ... object. Cette dernière est la classe parente universelle en Python.

Précédemment je disais que l'ordre est de gauche à droite de manière générale.
Prenons un exemple un peu plus complexe :

class A:
    pass

class B(A):
    pass

class C(A):
    pass

class D(B, C):
    pass

print(D.mro())

# [<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>]

Ici l'ordre ne sera pas D, B, C, A, A, object, la MRO utilise l'algorithme C3 qui maintient une cohérence lorsque il y a des ancêtres communs : D, B, C, A, object.

L'héritage a donc plusieurs avantages :

  • Réutilisabilité du code en évitant la duplication en partageant le code en commun via une classe de base

  • Extensibilité en ajoutant de nouvelles fonctionnalités avec la surcharge

  • Organisation en créant une structure logique

Pour aller plus loin sur le sujet de l'orienté objet et l'héritage, vous pouvez voir notre formation complète sur les concepts avancés de l'orienté objet.

Bravo, tu es prêt à passer à la suite

Rechercher sur le site

Formulaire de contact

Inscris-toi à Docstring

Pour commencer ton apprentissage.

Tu as déjà un compte ? Connecte-toi.