Nous énoncons ici quelques principes généraux de l'héritage qui se retrouvent dans la plupart des langages de programmation utilisant l'approche objet.
L'héritage est un mécanisme qui a été introduit dans la programmation objet dans le but d'éviter la réécriture inutile de code lorsqu'une classe n'est qu'un cas spécial d'une autre classe:
La classe plus spécialisée est appelée sous-classe (ou classe fille)et la classe plus générale, classe mère.
Lorsqu'une relation d'héritage est définie entre deux classes, la sous-classe hérite de tous les attributs et méthodes de la classe mère. C'est à dire que tout se passe comme si les attributs et méthodes de la classe mère avaient été explicitement définies pour la sous-classe.
Une sous-classe d'une classe donnée représente donc une sorte de cas particulier de cette classe qui possède des attributs et méthodes supplémentaires et spécifiques.
Notez que la relation d'héritage entre classes peut se faire à plusieurs niveaux: la classe mère d'une classe, peut également avoir une classe mère ... etc.
Prenons le cas d'un héritage à trois niveaux: la classe C hérite de la classe B, qui elle même hérite de la classe A:
Dans ce cas, la classe C héritera de tous les attributs et méthodes de la classe B, mais également de ceux de la classe A. A et B sont des classes ascendantes de la classe C et inversement B et C sont des classes descendantes de la classe A.
De manière générale, une classe hérite donc des attributs et méthodes de toutes ses classes ascendantes.
La redéfinition de méthode (ou overriding en anglais) est une notion liée à l'héritage: c'est le fait de pouvoir redéfinir une méthode déjà définie dans la classe mère.
Nous verrons à travers un exemple comment cela se traduit en Python.
Voilà comment déclarer qu'une classe B est une sous-classe d'une classe A :
class B ( A ) : .... }
En la déclarant de cette manière, la classe B hérite de toutes les membres publiques de la classe A. Attention :
L'exemple de projet qui nous servira à introduire les notions d'héritage et de redéfinition de méthode se trouve dans le dossier Exemple-Python-ProgObjet1/Formes.
Il contient la définition d'une classe Cercle comme sous-classe d'une autre classe nommée Forme.
Toute forme possède une aire. Un cercle est une forme spéciale qui possède également un rayon.
Pour éviter les confusions, une instance de la classe Forme sera appelée par la suite forme quelconque. Par contre, une forme sans autre précision désignera soit une forme quelconque soit un cercle.
La boucle de saisie des données propose deux choix:
Le premier choix , Forme quelconque permet de générer une forme quelconque en précisant simplement son aire. Le choix Cercle permet de générer un cercle de rayon donné. L'aire du cercle est calculée automatiquement.
Chaque création de forme, que ce soit par le premier choix ou par le deuxième, créée un objet de la classe Forme ou de la classe Cercle, ajoute cet objet dans une liste et affiche la liste.
Voici un exemple d'exécution où nous avons créé un cercle de rayon 10, puis une forme quelconque d'aire 400:
L'affichage de la liste des formes en mémoire décrit chaque forme sur une ligne. On y trouve l'indice de la forme dans la liste, sa classe (Forme ou Cercle), son aire et uniquement s'il s'agit d'un cercle, son rayon.
Commencons par la classe mère, c'est à dire la classe Forme dans notre exemple. Voici sa déclaration (pour l'instant nous ommettons les instructions contenues dans les méthodes afin de ne pas rentrer tout de suite dans les détails):
class Forme : TF = [] NF = 0 @staticmethod def Ajouter (f) : ... @staticmethod def Afficher_Les_Formes () : .... def __init__ (self,a) : self.Aire = a def Classe (self) : .... def Description (self) : ....
La classe Forme possède deux attributs statiques:
ainsi que deux méthodes statiques:
Une forme quelconque ne possède que l'attribut Aire. Le constructeur initialise cet attribut avec l'aire a passé en paramètre.
La classe Forme possède deux méthodes non statiques (ou "normales" si vous préférez) :
Voici la déclaration de la classe Cercle en tant que sous-classe de la classe Forme (en omettant comme avant le code des méthodes) :
class Cercle (Forme) : @staticmethod def CalculerAire (r) : ... def __init__ (self,r, a) : ... def Classe (self) : .... def Description (self) : ....
En tant que sous-classe de la classe Forme, la classe Cercle hérite de toutes ses méthodes publiques et membres statiques publiques (cliquez ici pour les voir dans le cadre droit).
Dans notre exemple les membres statiques sont tous publiques et sont donc tous hérités. Mais cela n'a pas beaucoup d'intérêt, à part que l'on pourra écrire Cercle.TF à la place de Forme.TF ou Cercle.Ajouter(f) au lieu de Forme.Ajouter(f).
Le constructeur ainsi que les méthodes Classe et Description ne sont pas héritées car ces méthodes sont redéfinies dans la classe Cercle.
Ces méthodes "masquent" en quelque sorte celles de la classe mère. Lorsqu'une méthode redéfinie dans la classe fille est appliquée à un objet de cette classe, c'est la méthode de la classe fille qui est exécutée. Par exemple, pour un objet c de la classe Cercle, l'expression c.Description () est un appel de la méthode Description de la classe Cercle.
La classe Cercle possède également une méthode qui n'est ni héritée ni redéfinie: il s'agit de la méthode CalculerAire. Cette méthode statique retourne l'aire d'un cercle de rayon donné.
Rappelons qu'une méthode est redéfinie si elle est déclarée à la fois dans la classe mère et dans la classe fille. Dans notre exemple, nous en avons trois: il s'agit du constructeur et des méthodes Classe et Description.
Lorsqu'une méthode est redéfinie, on y retrouve souvent le code de la méthode de la classe mère suivi d'un code spécifique à la classe fille:
class classe mère ( ): def méthode redéfinie (self,....) : code commun class classe fille (classe mère) : def méthode redéfinie (self,....) : code commun code spécifique
Le mot clé super permet d'éviter de réécrire inutilement le code commun en appelant la méthode de la classe mère dans celle de la classe fille:
class classe mère ( ): def méthode redéfinie (self,....) : code commun class classe fille (classe mère) : def méthode redéfinie (self,....) : super().méthode redéfinie (self,....) code spécifique
Le fait de préfixer l'appel de la méthode par super() indique à l'interpréteur que la méthode à exécuter est celle de la classe mère. A présent voyons comment appliquer cela dans la pratique avec les méthodes redéfinies de notre exemple.
Voici le code du constructeur :
def __init__ (self,r, a) : super().__init__(a) self.Rayon = r
Ici super est utilisé pour appeler le constructeur de la classe mère. Autrement dit, le code précédent est équivalent à:
def __init__ (self,r, a) : self.Aire = a self.Rayon = r
Nous n'avons rien gagné en nombre de lignes de code. Mais l'intérêt de super n'est pas seulement dans la concision du code. En particulier, le fait de l'utiliser dans le constructeur de la classe fille permet de réaliser l'héritage des attributs qui manque à Python (à condition de s'imposer de toujours initialiser tous les attributs dans les constructeurs !).
La méthode Classe n'utilise pas super car le code de cette méthode dans la classe fille n'est pas une extension de celui de la classe mère:
def Classe (self) : return "Forme"
def Classe (self) : return "Cercle"
La méthode Description retourne une chaine de caractères décrivant une forme de classe à priori inconnue. En redéfinissant cette méthode, nous lui donnons deux comportements possibles: pour une forme quelconque, elle donnera son aire et pour un cercle, son aire et son rayon. Ceci nous permettra en particulier d'afficher de manière élégante toutes les formes en mémoire (donc contenues dans la liste TF) par une simple boucle for ne contenant aucun test:
@staticmethod def Afficher_Les_Formes () : if Forme.NF==0 : print("\n\tLA LISTE DES FORMES EST VIDE") else : print("\n\tLISTE DES FORMES EN MEMOIRE") for i in range(0,Forme.NF) : print("\t",i,": "+ Forme.TF[i].Description())
Voyons à présent comment sont codées les deux versions de la méthode Description.
Dans la classe Forme, elle est déclarée comme suit:
def Description (self) : return self.Classe()+" d'aire "+ str(int(self.Aire))
Dans la classe Cercle, Description est déclarée comme ceci:
def Description (self) : return super().Description()+" et de rayon "+ str(self.Rayon)
La chaine de caractères construite utilise la méthode Description de la classe Forme. Pour préciser qu'il s'agit bien de celle de la classe Forme, nous avons préfixé le nom de la méthode par super().
Voyons à présent comment ces méthodes s'exécutent selon la classe de l'objet auquel elles s'appliquent.
Considérons un élément TF[i] de la liste TF. Cet élément peut être un objet de la classe Forme ou de la classe Cercle.
Dans le premier cas,TF[i].Description () exécutera la méthode Description de la classe Forme. La méthode Classe retournera dans ce cas la chaine "Forme". En supposant que l'aire de cette forme soit égale à 100, la chaine retournée sera "Forme d'aire 100".
Dans le deuxième cas, TF[i].Description () exécutera la méthode Description de la classe Cercle. La méthode Classe() retournera dans ce cas la chaine "Cercle". Supposons que ce soit un cercle de rayon 2 et d'aire 12. super().Description() retournera la chaine "Cercle d'aire 12". Le résultat retourné sera donc "Cercle d'aire 12 et de rayon 2".