Ce cours est visible gratuitement en ligne.

Paperback available in this course

Ce cours existe en eBook.

Certificate of achievement available at the end this course

Got it!
Apprenez à programmer en Python

Apprenez à programmer en Python

Last updated on Monday, September 8, 2014
  • 4 semaines
  • Facile

L'héritage

J'entends souvent dire qu'un langage de programmation orienté objet n'incluant pas l'héritage serait incomplet, sinon inutile. Après avoir découvert par moi-même cette fonctionnalité et les techniques qui en découlent, je suis forcé de reconnaître que sans l'héritage, le monde serait moins beau !

Qu'est-ce que cette fonctionnalité a de si utile ?
Nous allons le voir, bien entendu. Et je vais surtout essayer de vous montrer des exemples d'applications. Car très souvent, quand on découvre l'héritage, on ne sait pas trop quoi en faire…
Ne vous attendez donc pas à un chapitre où vous n'allez faire que coder. Vous allez devoir vous pencher sur de la théorie et travailler sur quelques exemples de modélisation. Mais je vous guide, ne vous inquiétez pas !

Pour bien commencer

Je ne vais pas faire durer le suspense plus longtemps : l'héritage est une fonctionnalité objet qui permet de déclarer que telle classe sera elle-même modelée sur une autre classe, qu'on appelle la classe parente, ou la classe mère. Concrètement, si une classe bhérite de la classe a, les objets créés sur le modèle de la classe b auront accès aux méthodes et attributs de la classe a.

Et c'est tout ? Cela ne sert à rien !

Non, ce n'est pas tout, et si, cela sert énormément mais vous allez devoir me laisser un peu de temps pour vous en montrer l'intérêt.

La première chose, c'est que la classe b dans notre exemple ne se contente pas de reprendre les méthodes et attributs de la classe a : elle va pouvoir en définir d'autres. D'autres méthodes et d'autres attributs qui lui seront propres, en plus des méthodes et attributs de la classe a. Et elle va pouvoir également redéfinir les méthodes de la classe mère.

Prenons un exemple simple : on a une classe Animal permettant de définir des animaux. Les animaux tels que nous les modélisons ont certains attributs (le régime : carnivore ou herbivore) et certaines méthodes (manger, boire, crier…).

On peut maintenant définir une classe Chien qui hérite de Animal, c'est-à-dire qu'elle reprend ses méthodes. Nous allons voir plus bas ce que cela implique exactement.

Si vous ne voyez pas très bien dans quel cas on fait hériter une classe d'une autre, faites le test :

  • on fait hériter la classe Chien de Animal parce qu'un chien est un animal ;

  • on ne fait pas hériter Animal de Chien parce qu'Animal n'est pas un Chien.

Sur ce modèle, vous pouvez vous rendre compte qu'une voiture est un véhicule. La classe Voiture pourrait donc hériter de Vehicule.

Intéressons-nous à présent au code.

L'héritage simple

On oppose l'héritage simple, dont nous venons de voir les aspects théoriques dans la section précédente, à l'héritage multiple que nous verrons dans la prochaine section.

Il est temps d'aborder la syntaxe de l'héritage. Nous allons définir une première classe A et une seconde classe B qui hérite de A.

class A:
    """Classe A, pour illustrer notre exemple d'héritage"""
    pass # On laisse la définition vide, ce n'est qu'un exemple

class B(A):
    """Classe B, qui hérite de A.
    Elle reprend les mêmes méthodes et attributs (dans cet exemple, la classe
    A ne possède de toute façon ni méthode ni attribut)"""
    
    pass

Vous pourrez expérimenter par la suite sur des exemples plus constructifs. Pour l'instant, l'important est de bien noter la syntaxe qui, comme vous le voyez, est des plus simples : class MaClasse(MaClasseMere):. Dans la définition de la classe, entre le nom et les deux points, vous précisez entre parenthèses la classe dont elle doit hériter. Comme je l'ai dit, dans un premier temps, toutes les méthodes de la classe A se retrouveront dans la classe B.

J'ai essayé de mettre des constructeurs dans les deux classes mais, dans la classe fille, je ne retrouve pas les attributs déclarés dans ma classe mère, c'est normal ?

Tout à fait. Vous vous souvenez quand je vous ai dit que les méthodes étaient définies dans la classe, alors que les attributs étaient directement déclarés dans l'instance d'objet ? Vous le voyez bien de toute façon : c'est dans le constructeur qu'on déclare les attributs et on les écrit tous dans l'instance self.

Quand une classe B hérite d'une classe A, les objets de type B reprennent bel et bien les méthodes de la classe A en même temps que celles de la classe B. Mais, assez logiquement, ce sont celles de la classe B qui sont appelées d'abord.

Si vous faites objet_de_type_b.ma_methode(), Python va d'abord chercher la méthode ma_methode dans la classe B dont l'objet est directement issu. S'il ne trouve pas, il va chercher récursivement dans les classes dont hérite B, c'est-à-dire A dans notre exemple. Ce mécanisme est très important : il induit que si aucune méthode n'a été redéfinie dans la classe, on cherche dans la classe mère. On peut ainsi redéfinir une certaine méthode dans une classe et laisser d'autres directement hériter de la classe mère.

Petit code d'exemple :

class Personne:
    """Classe représentant une personne"""
    def __init__(self, nom):
        """Constructeur de notre classe"""
        self.nom = nom
        self.prenom = "Martin"
    def __str__(self):
        """Méthode appelée lors d'une conversion de l'objet en chaîne"""
        return "{0} {1}".format(self.prenom, self.nom)

class AgentSpecial(Personne):
    """Classe définissant un agent spécial.
    Elle hérite de la classe Personne"""
    
    def __init__(self, nom, matricule):
        """Un agent se définit par son nom et son matricule"""
        self.nom = nom
        self.matricule = matricule
    def __str__(self):
        """Méthode appelée lors d'une conversion de l'objet en chaîne"""
        return "Agent {0}, matricule {1}".format(self.nom, self.matricule)

Vous voyez ici un exemple d'héritage simple. Seulement, si vous essayez de créer des agents spéciaux, vous risquez d'avoir de drôles de surprises :

>>> agent = AgentSpecial("Fisher", "18327-121")
>>> agent.nom
'Fisher'
>>> print(agent)
Agent Fisher, matricule 18327-121
>>> agent.prenom
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'AgentSpecial' object has no attribute 'prenom'
>>>

Argh… mais tu n'avais pas dit qu'une classe reprenait les méthodes et attributs de sa classe mère ?

Si. Mais en suivant bien l'exécution, vous allez comprendre : tout commence à la création de l'objet. Quel constructeur appeler ? S'il n'y avait pas de constructeur défini dans notre classe AgentSpecial, Python appellerait celui de Personne. Mais il en existe bel et bien un dans la classe AgentSpecial et c'est donc celui-ci qui est appelé. Dans ce constructeur, on définit deux attributs, nom et matricule. Mais c'est tout : le constructeur de la classe Personne n'est pas appelé, sauf si vous l'appelez explicitement dans le constructeur d'AgentSpecial.

Dans le premier chapitre, je vous ai expliqué que mon_objet.ma_methode() revenait au même que MaClasse.ma_methode(mon_objet). Dans notre méthode ma_methode, le premier paramètre self sera mon_objet. Nous allons nous servir de cette équivalence. La plupart du temps, écrire mon_objet.ma_methode() suffit. Mais dans une relation d'héritage, il peut y avoir, comme nous l'avons vu, plusieurs méthodes du même nom définies dans différentes classes. Laquelle appeler ? Python choisit, s'il la trouve, celle définie directement dans la classe dont est issu l'objet, et sinon parcourt la hiérarchie de l'héritage jusqu'à tomber sur la méthode. Mais on peut aussi se servir de la notation MaClasse.ma_methode(mon_objet) pour appeler une méthode précise d'une classe précise. Et cela est utile dans notre cas :

class Personne:
    """Classe représentant une personne"""
    def __init__(self, nom):
        """Constructeur de notre classe"""
        self.nom = nom
        self.prenom = "Martin"
    def __str__(self):
        """Méthode appelée lors d'une conversion de l'objet en chaîne"""
        return "{0} {1}".format(self.prenom, self.nom)

class AgentSpecial(Personne):
    """Classe définissant un agent spécial.
    Elle hérite de la classe Personne"""
    
    def __init__(self, nom, matricule):
        """Un agent se définit par son nom et son matricule"""
        # On appelle explicitement le constructeur de Personne :
        Personne.__init__(self, nom)
        self.matricule = matricule
    def __str__(self):
        """Méthode appelée lors d'une conversion de l'objet en chaîne"""
        return "Agent {0}, matricule {1}".format(self.nom, self.matricule)

Si cela vous paraît encore un peu vague, expérimentez : c'est toujours le meilleur moyen. Entraînez-vous, contrôlez l'écriture des attributs, ou revenez au premier chapitre de cette partie pour vous rafraîchir la mémoire au sujet du paramètre self, bien qu'à force de manipulations vous avez dû comprendre l'idée.

Reprenons notre code de tout à l'heure qui, cette fois, passe sans problème :

>>> agent = AgentSpecial("Fisher", "18327-121")
>>> agent.nom
'Fisher'
>>> print(agent)
Agent Fisher, matricule 18327-121
>>> agent.prenom
'Martin'
>>>

Cette fois, notre attribut prenom se trouve bien dans notre agent spécial car le constructeur de la classe AgentSpecial appelle explicitement celui de Personne.

Vous pouvez noter également que, dans le constructeur d'AgentSpecial, on n'instancie pas l'attribut nom. Celui-ci est en effet écrit par le constructeur de la classe Personne que nous appelons en lui passant en paramètre le nom de notre agent.

Notez que l'on pourrait très bien faire hériter une nouvelle classe de notre classe Personne, la classe mère est souvent un modèle pour plusieurs classes filles.

Petite précision

Dans le chapitre précédent, je suis passé très rapidement sur l'héritage, ne voulant pas trop m'y attarder et brouiller les cartes inutilement. Mais j'ai expliqué brièvement que toutes les classes que vous créez héritent de la classe object. C'est elle, notamment, qui définit toutes les méthodes spéciales que nous avons vues au chapitre précédent et qui connaît, bien mieux que nous, le mécanisme interne de l'objet. Vous devriez un peu mieux, à présent, comprendre le code du chapitre précédent. Le voici, en substance :

def __setattr__(self, nom_attribut, valeur_attribut):
        """Méthode appelée quand on fait objet.attribut = valeur"""
        print("Attention, on modifie l'attribut {0} de l'objet !".format(nom_attribut))
        object.__setattr__(self, nom_attribut, valeur_attribut)

En redéfinissant la méthode __setattr__, on ne peut, dans le corps de cette méthode, modifier les valeurs de nos attributs comme on le fait habituellement (self.attribut = valeur) car alors, la méthode s'appellerait elle-même. On fait donc appel à la méthode __setattr__ de la classe object, cette classe dont héritent implicitement toutes nos classes. On est sûr que la méthode de cette classe sait écrire une valeur dans un attribut, alors que nous ignorons le mécanisme et que nous n'avons pas besoin de le connaître : c'est la magie du procédé, une fois qu'on a bien compris le principe !

Deux fonctions très pratiques

Python définit deux fonctions qui peuvent se révéler utiles dans bien des cas : issubclass et isinstance.

issubclass

Comme son nom l'indique, elle vérifie si une classe est une sous-classe d'une autre classe. Elle renvoie True si c'est le cas, False sinon :

>>> issubclass(AgentSpecial, Personne) # AgentSpecial hérite de Personne
True
>>> issubclass(AgentSpecial, object)
True
>>> issubclass(Personne, object)
True
>>> issubclass(Personne, AgentSpecial) # Personne n'hérite pas d'AgentSpecial
False
>>>
isinstance

isinstance permet de savoir si un objet est issu d'une classe ou de ses classes filles :

>>> agent = AgentSpecial("Fisher", "18327-121")
>>> isinstance(agent, AgentSpecial) # Agent est une instance d'AgentSpecial
True
>>> isinstance(agent, Personne) # Agent est une instance héritée de Personne
True
>>>

Ces quelques exemples suffisent, je pense. Peut-être devrez-vous attendre un peu avant de trouver une utilité à ces deux fonctions mais ce moment viendra.

L'héritage multiple

Python inclut un mécanisme permettant l'héritage multiple. L'idée est en substance très simple : au lieu d'hériter d'une seule classe, on peut hériter de plusieurs.

Ce n'est pas ce qui se passe quand on hérite d'une classe qui hérite elle-même d'une autre classe ?

Pas tout à fait. La hiérarchie de l'héritage simple permet d'étendre des méthodes et attributs d'une classe à plusieurs autres, mais la structure reste fermée. Pour mieux comprendre, considérez l'exemple qui suit.

On peut s'asseoir dans un fauteuil. On peut dormir dans un lit. Mais on peut s'asseoir et dormir dans certains canapés (la plupart en fait, avec un peu de bonne volonté). Notre classe Fauteuil pourra hériter de la classe ObjetPourSAsseoir et notre classe Lit, de notre classe ObjetPourDormir. Mais notre classe Canape alors ? Elle devra logiquement hériter de nos deux classes ObjetPourSAsseoir et ObjetPourDormir. C'est un cas où l'héritage multiple pourrait se révéler utile.

Assez souvent, on utilisera l'héritage multiple pour des classes qui ont besoin de certaines fonctionnalités définies dans une classe mère. Par exemple, une classe peut produire des objets destinés à être enregistrés dans des fichiers. On peut faire hériter de cette classe toutes celles qui produiront des objets à enregistrer dans des fichiers. Mais ces mêmes classes pourront hériter d'autres classes incluant, pourquoi pas, d'autres fonctionnalités.

C'est une des utilisations de l'héritage multiple et il en existe d'autres. Bien souvent, l'utilisation de cette fonctionnalité ne vous semblera évidente qu'en vous penchant sur la hiérarchie d'héritage de votre programme. Pour l'instant, je vais me contenter de vous donner la syntaxe et un peu de théorie supplémentaire, en vous encourageant à essayer par vous-mêmes :

class MaClasseHeritee(MaClasseMere1, MaClasseMere2):

Vous pouvez faire hériter votre classe de plus de deux autres classes. Au lieu de préciser, comme dans les cas d'héritage simple, une seule classe mère entre parenthèses, vous en indiquez plusieurs, séparées par des virgules.

Recherche des méthodes

La recherche des méthodes se fait dans l'ordre de la définition de la classe. Dans l'exemple ci-dessus, si on appelle une méthode d'un objet issu de MaClasseHeritee, on va d'abord chercher dans la classe MaClasseHeritee. Si la méthode n'est pas trouvée, on la cherche d'abord dans MaClasseMere1. Encore une fois, si la méthode n'est pas trouvée, on cherche dans toutes les classes mères de la classe MaClasseMere1, si elle en a, et selon le même système. Si, encore et toujours, on ne trouve pas la méthode, on la recherche dans MaClasseMere2 et ses classes mères successives.

C'est donc l'ordre de définition des classes mères qui importe. On va chercher la méthode dans les classes mères de gauche à droite. Si on ne trouve pas la méthode dans une classe mère donnée, on remonte dans ses classes mères, et ainsi de suite.

Retour sur les exceptions

Depuis la première partie, nous ne sommes pas revenus sur les exceptions. Toutefois, ce chapitre me donne une opportunité d'aller un peu plus loin.

Les exceptions sont non seulement des classes, mais des classes hiérarchisées selon une relation d'héritage précise.

Cette relation d'héritage devient importante quand vous utilisez le mot-clé except. En effet, le type de l'exception que vous précisez après est intercepté… ainsi que toutes les classes qui héritent de ce type.

Mais comment fait-on pour savoir qu'une exception hérite d'autres exceptions ?

Il y a plusieurs possibilités. Si vous vous intéressez à une exception en particulier, consultez l'aide qui lui est liée.

Help on class AttributeError in module builtins:

class AttributeError(Exception)
 |  Attribute not found.
 |
 |  Method resolution order:
 |      AttributeError
 |      Exception
 |      BaseException
 |      object

Vous apprenez ici que l'exception AttributeError hérite de Exception, qui hérite elle-même de BaseException.

Vous pouvez également retrouver la hiérarchie des exceptions built-in sur le site de Python.

Ne sont répertoriées ici que les exceptions dites built-in. D'autres peuvent être définies dans des modules que vous utiliserez et vous pouvez même en créer vous-mêmes (nous allons voir cela un peu plus bas).

Pour l'instant, souvenez-vous que, quand vous écrivez except TypeException, vous pourrez intercepter toutes les exceptions du type TypeException mais aussi celles des classes héritées de TypeException.

La plupart des exceptions sont levées pour signaler une erreur… mais pas toutes. L'exception KeyboardInterupt est levée quand vous interrompez votre programme, par exemple avec CTRL + C. Si bien que, quand on souhaite intercepter toutes les erreurs potentielles, on évitera d'écrire un simple except: et on le remplacera par except Exception:, toutes les exceptions « d'erreurs » étant dérivées de Exception.

Création d'exceptions personnalisées

Il peut vous être utile de créer vos propres exceptions. Puisque les exceptions sont des classes, comme nous venons de le voir, rien ne vous empêche de créer les vôtres. Vous pourrez les lever avec raise, les intercepter avec except.

Se positionner dans la hiérarchie

Vos exceptions doivent hériter d'une exception built-in proposée par Python. Commencez par parcourir la hiérarchie des exceptions built-in pour voir si votre exception peut être dérivée d'une exception qui lui serait proche. La plupart du temps, vous devrez choisir entre ces deux exceptions :

  • BaseException : la classe mère de toutes les exceptions. La plupart du temps, si vous faites hériter votre classe de BaseException, ce sera pour modéliser une exception qui ne sera pas foncièrement une erreur, par exemple une interruption dans le traitement de votre programme.

  • Exception : c'est de cette classe que vos exceptions hériteront la plupart du temps. C'est la classe mère de toutes les exceptions « d'erreurs ».

Si vous pouvez trouver, dans le contexte, une exception qui se trouve plus bas dans la hiérarchie, c'est toujours mieux.

Que doit contenir notre classe exception ?

Deux choses : un constructeur et une méthode __str__ car, au moment où l'exception est levée, elle doit être affichée. Souvent, votre constructeur ne prend en paramètre que le message d'erreur et la méthode __str__ renvoie ce message :

class MonException(Exception):
    """Exception levée dans un certain contexte… qui reste à définir"""
    def __init__(self, message):
        """On se contente de stocker le message d'erreur"""
        self.message = message
    def __str__(self):
        """On renvoie le message"""
        return self.message

Cette exception s'utilise le plus simplement du monde :

>>> raise MonException("OUPS... j'ai tout cassé")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
__main__.MonException: OUPS... j'ai tout cassé
>>>

Mais vos exceptions peuvent aussi prendre plusieurs paramètres à l'instanciation :

class ErreurAnalyseFichier(Exception):
    """Cette exception est levée quand un fichier (de configuration)
    n'a pas pu être analysé.
    
    Attributs :
        fichier -- le nom du fichier posant problème
        ligne -- le numéro de la ligne posant problème
        message -- le problème proprement dit"""
    
    def __init__(self, fichier, ligne, message):
        """Constructeur de notre exception"""
        self.fichier = fichier
        self.ligne = ligne
        self.message = message
    def __str__(self):
        """Affichage de l'exception"""
        return "[{}:{}]: {}".format(self.fichier, self.ligne, \
                self.message)

Et pour lever cette exception :

>>> raise ErreurAnalyseFichier("plop.conf", 34,
...         "Il manque une parenthèse à la fin de l'expression")
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
__main__.ErreurAnalyseFichier: [plop.conf:34]: il manque une parenthèse à la fin de l'expression
>>>

Voilà, ce petit retour sur les exceptions est achevé. Si vous voulez en savoir plus, n'hésitez pas à consulter la documentation Python concernant les exceptions ainsi que celle sur les exceptions personnalisées.

En résumé

  • L'héritage permet à une classe d'hériter du comportement d'une autre en reprenant ses méthodes.

  • La syntaxe de l'héritage est class NouvelleClasse(ClasseMere):.

  • On peut accéder aux méthodes de la classe mère directement via la syntaxe : ClasseMere.methode(self).

  • L'héritage multiple permet à une classe d'hériter de plusieurs classes mères.

  • La syntaxe de l'héritage multiple s'écrit donc de la manière suivante : class NouvelleClasse(ClasseMere1, ClasseMere2, ClasseMereN):.

  • Les exceptions définies par Python sont ordonnées selon une hiérarchie d'héritage.

Example of certificate of achievement
Example of certificate of achievement