Présentation du concept d’héritage en Python orienté objet
En programmation orientée objet, “hériter” signifie “avoir également accès à”. Lorsqu’on dit qu’un objet “hérite” des méthodes de la classe qui l’a défini, cela signifie que l’objet peut utiliser ces méthodes; qu’il y a accès.
La notion d’héritage va être particulièrement intéressante lorsqu’on va l’implémenter entre deux classes. En Python, nous allons en effet pouvoir créer des “sous-classes” ou des classes “enfants” à partir de classes de base ou classes “parentes”.
La syntaxe pour définir une sous-classe à partir d’une classe de base est la suivante :
Ici, Utilisateur
est notre classe de base et Client
est notre sous-classe.
Par défaut, la sous-classe hérite de toutes les variables et fonctions de la classe parent (et notamment de sa fonction __init__()
) et peut également définir ses propres variables et fonctions. On va ensuite pouvoir instancier la classe enfant pour créer de nouveaux objets et ces objets vont avoir accès aux variables et fonctions définies à la fois dans la sous-classe et dans la classe de base.
Ce principe d’héritage va nous permettre de créer des classes de base qui vont définir des fonctionnalités communes à plusieurs classes puis des sous-classes qui vont hériter de ces fonctionnalités et pouvoir également définir leurs propres fonctionnalités.
Cela permet in-fine d’obtenir un code plus modulable, mieux organisé, plus clair et plus concis qui sont des objectifs majeurs pour tout bon développeur.
La surcharge des méthodes de classe
“Surcharger” une méthode signifie la redéfinir d’une façon différente. En Python, les classes filles ou sous-classes vont pouvoir surcharger les méthodes héritées de leur classe parent et également pouvoir définir des variables de même nom que celles de leur classe parent.
On ne parlera de surcharge que pour les méthodes car dans le cas des variables définir une variable avec le même nom dans la sous-classe correspond finalement à créer une variable locale à la sous-classe en plus de celle “globale” (celle disponible dans la classe de base) mais ces deux variables continuent d’exister à part entière et à être différente tandis que lorsqu’on réécrit une méthode la nouvelle méthode remplace véritablement l’ancienne pour les objets de la sous-classe.
Réécrivons notre classe Client()
afin de définir certaines variables locales et de surcharger les méthodes de la classe mère Utilisateur
:
Notez qu’en pratique, on ne voudra souvent pas simplement réécrire l’intégralité du code d’une classe mère dans une classe fille (cela signifierait que nos classes sont mal construites) mais on voudra simplement “étendre” la méthode c’est à dire lui rajouter des instructions spécifiques pour une classe fille.
On va pouvoir faire cela en appelant la méthode de notre classe mère depuis notre classe fille avec la syntaxe ClasseDeBase.nomDeMethode()
, ce qui va nous permettre de récupérer l’intégralité du code de la classe mère.
Modifions à nouveau notre classe Client
afin d’étendre la fonction __init__()
de la classe mère :
Les tests d’héritage Python
A ce niveau du cours, vous pourriez (et devriez) vous poser la question suivante : comment l’objet fait pour déterminer si il doit utiliser les variables ou méthodes de sa classe ou de la classe mère dont sa classe hérite ?
La réponse à cette question est très simple : Python va rechercher les variables et méthodes dans un ordre précis.
Lorsqu’on tente d’afficher le contenu d’un attribut de données ou d’appeler une méthode depuis un objet, Python va commencer par chercher si la variable ou la fonction correspondantes se trouvent dans la classe qui a créé l’objet. Si c’est le cas, il va les utiliser. Si ce n’est pas le cas, il va chercher dans la classe mère de la classe de l’objet si cette classe possède une classe mère. Si il trouve ce qu’il cherche, il utilisera cette variable ou fonction. Si il ne trouve pas, il cherchera dans la classe mère de la classe mère si elle existe et etc.
Python va en fait “remonter” le long de la chaine d’héritage des classes jusqu’à trouver l’information demandée. Si. Elle n’est jamais trouvée, il renverra finalement une erreur.
Notez ici que Python nous fournit également deux fonctions pour nous permettre de tester le type d’une instance et l’héritage d’une classe.
La fonction isinstance()
permet de tester le type d’une instance, c’est-à-dire le type d’une objet, c’est-à-dire permet de savoir si un objet appartient à une certaine classe ou pas. On va lui passer en arguments l’objet dont on souhaite tester le type et le type qui doit servir de test. Cette fonction renverra True
si l’objet est bien du type passé en second argument ou si il est d’un sous-type dérivé de ce type ou False
sinon.
La fonction issubclass()
permet de tester l’héritage d’une classe, c’est-à-dire permet de savoir si une classe hérite bien d’une autre classe ou pas. On va lui passer en arguments la classe à tester ainsi qu’une autre classe dont on aimerait savoir si c’est une classe mère de la classe passée en premier argument ou pas. La fonction renverra True
si c’st le cas ou False
sinon.
Ces deux fonctions sont très utiles pour tester rapidement les liens hiérarchiques entre certaines classes et objets et peuvent aider à comprendre plus facilement un script complexe qui nous aurait été passé.
Le polymorphisme en Python orienté objet
“Polymorphisme” signifie littéralement “plusieurs formes”. Dans le contexte de la programmation orientée objet, le polymorphisme est un concept qui fait référence à la capacité d’une variable, d’une fonction ou d’un objet à prendre plusieurs formes, c’est-à-dire à sa capacité de posséder plusieurs définitions différentes.
Pour bien comprendre ce concept, imaginons qu’on définisse une classe nommée Animaux
qui possède des fonctions comme seNourrir()
, seDeplacer()
, etc. Notre classe va pouvoir ressembler à ça :
Ici, j’utilise un nouveau mot clef pass
qui me sert à créer une fonction vide. En effet, le mot clef pass
ne fait strictement rien en Python. On est obligés de l’utiliser pour créer une fonction vide car si on n’écrit rien dans notre fonction l’interpréteur Python va renvoyer une erreur.
Ma classe Animaux
dispose donc d’une fonction seDeplacer()
qui ne contient pas d’instruction. Maintenant, nous allons créer des sous-classes de Animaux
pour différents animaux : Chien
, Aigle
et Dauphin
par exemple.
Ces trois sous classes vont par défaut hériter des membres de leur classe mère Animaux
et notamment de la méthode seDeplacer()
. Ici, chacune de nos sous classes va implémenter cette méthode différemment, c’est-à-dire va la définir différemment.
Pour ma classe Chien
par exemple, la méthode seDeplacer()
va renvoyer une valeur “courir” tandis que pour Aigle
cette méthode va renvoyer une valeur “voler”. Pour Dauphin
, seDeplacer()
renverra “nager”.
Ceci est un exemple typique de polymorphisme : plusieurs sous-classes héritent d’une méthode d’une classe de base qu’ils implémentent de manière différente.
Le polymorphisme permet également in-fine d’obtenir un code plus clair, plus lisible et plus cohérent : on va pouvoir fournir des définitions de fonctions vides dans une classe de base afin de laisser des sous-classes implémenter (définir) ces fonctions de différentes manières.
Héritage multiple
Pour terminer cette leçon sur l’héritage et le polymorphisme, il faut savoir que Python gère également une forme d’héritage multiple.
On parle d’héritage multiple en programmation orientée objet lorsqu’une sous-classe peut hériter de plusieurs classes mères différentes.
Dans la pratique, l’héritage multiple est une chose très difficile à mettre en place au niveau du langage puisqu’il faut prendre en charge les cas où plusieurs classes mères définissent les mêmes variables et fonctions et définir une procédure pour indiquer de quelle définition la sous-classe héritera.
En Python, dans la majorité des cas, l’héritage va se faire selon l’ordre des classes mères indiquées et cela de manière récursive. Imaginons qu’une sous-classe AGrave
hérite de trois classes A
, Accent
et Abracadabra
dans cet ordre et que la classe A
hérite elle même de la classe Alphabet
tandis que Abracadabra
hérite de Mot
On crée un objet en instanciant notre classe AGrave
et on appelle une méthode depuis notre objet. Python va à priori commencer par chercher la méthode dans AGrave
, puis si il ne la trouve pas cherchera dans A
. S’il ne la trouve pas il cherchera ensuite dans Alphabet
, puis dans Accent
, puis dans Abracadabra
et finalement dans Mot
.
Regardez plutôt le code suivant qui illustre bien cette situation :
En réalité, Python utilise un algorithme relativement complexe qui détermine l’ordre d’appel (method resolution order, ou MRO en anglais) de manière dynamique, c’est-à-dire en fonction des relations entre les différentes classes.
Dans notre exemple, si les classes A
, Accent
et Abracadabra
avaient eu des parents en commun, l’ordre aurait été beaucoup plus complexe à calculer. Ce genre de situations arrive très peu fréquemment et est à éviter; un bon développeur privilégiant toujours la lisibilité de son code et donc je n’en parlerai pas plus. Si vous souhaitez plus d’informations à ce sujet, je vous invite à vous renseigner sur l’algorithme C3 utilisé par Python pour déterminer le MRO.