5.4. heritage#
Objectifs
Dans ce cours vous apprendrez à étendre le code d’une classe en la dérivant en une autre classe. Vous apprendrez enrichir et redéfinir le comportement d’une classe mère dans une classe fille.
5.4.1. Ré-utilisation de code#
L’ingénierie informatique nécessite la correction permanente de bogues. A partir d’une certaine taille, un logiciel contiendra toujours quelques erreurs et de nouvelles erreurs apparaîtrons au fil de son utilisation et des évolutions du matériel et des dépendances logiciels. Le code d’un projet livré doit donc être maintenu.
La notion d’héritage naît de la volonté de réutiliser et étendre du code à maintenir.
Imaginons que nous sommes en train de coder un jeu très similaire à un jeu que nous avons déjà codé, finalisé et livré. Comment alors réutiliser le code existant pour le faire évoluer, gagnant ainsi du temps dans le développement de notre nouveau jeu? La première idée qui nous vient à l’esprit est de recopier les modules concernés dans notre nouveau projet. Ce qui serait dommage alors, est que cela nous amènerai maintenir deux codes identiques ou du moins très proches. La découverte et la résolution d’un bogue dans l’un des projet demanderait alors de propager le correctif dans l’autre projet pour assurer sa bonne maintenance des deux. Le problème serait alors de se rappeler quel code a été recopié dans quel projet pour savoir si il y a des correctifs a propager. Avec seulement 2 projets et un seul développeur, cette mission est difficile. On risque d’oublier rapidement quel code a été recopié. En transposant ce cas de figure, dans une équipe avec plusieurs développeurs sur une ou plusieurs applications partageant des briques logicielles, on se rend compte que l’équation devient impossible à résoudre : Il faut réutiliser le code pour gagner en productivité; mais : Il n’est pas souhaitable dupliquer le code réutilisé car ça le rend impossible à maintenir.
La programmation orientée objet propose alors d’étendre le code plutôt que de le modifier. C’est le principe d’ouverture/fermeture. On pense les classes comme des unités qui encapsulent du code fermé à la modification mais ouvert à l’extension si on les récupèrent dans un futur projet.
L’héritage est le mécanisme qui permet l’extension des classes.
#module example_encapsulation.py
class Ball:
def __init__(self,x,y):
self.__x=x
self.__y=y
def __repr__(self):
msg="ball position=("+str(self.__x)+";"+str(self.__y)+")"
return msg
def get_x(self): return self.__x
def get_y(self): return self.__y
def set_x(self,value): self.__x= value
def set_y(self,value): self.__y= value
def show(self):
x=str(int(self.__x))
y=str(int(self.__y))
sys.stdout.write("u\001b["+y+";"+x+"H"+"O")
if __name__=="__main__":
b=Ball(10,3)
b.show()
#examples_inheritance.py de mon nouveau projet
from example_encpsulation import Ball
class BeachBall(Ball): pass
if __name__=="__main__":
b=Ball(10,3)
b.show()
bb=BeachBall(1,2)
bb.show()
Nous avons ici repris la classe Ball
pour en faire une classe BeachBall
.
On voit que l’objet bb
peut utiliser la méthode show()
qui était définie au niveau de la classe Ball
Un objet du type BeachBall
est de type Ball
.
Pour le vérifier :
Voyons maintenant comment ajouter des propriétés (enrichissement) dans ce code étendu.
5.4.2. Enrichissement#
Cette nouvelle classe BeachBall
reprend les caractéristique de la classe Ball
et y ajoute une couleur!
Elle propose aussi une nouvelle méthode hide()
qui a pour effet de mettre sa couleur à la valeur 0
.
Et une méthode fall()
qui a pour effet de mettre sa valeur y
à 0
Tout d’abord il faut créer un constructeur et penser à appeler le constructeur de la classe de laquelle on hérite.
Ce nous nouveau constructeur permet d’ajouter des choses :
#examples_inheritance.py de mon nouveau projet
from example_encpsulation import Ball
class BeachBall(Ball):
def __init__(self,x,y,color):
Ball.__init__(self,x,y)
self.__color=color
def get_color(self): return self.__color
def set_color(self, value): self.__color=value
def hide(self):
self.__color=0
if __name__=="__main__":
bb=BeachBall(x=1,y=2,color=3)
bb.hide()
print(bb)
print(bb.__dict__)
Le fait de proposer de nouveau attributs, de nouvelles associations ou de nouvelles méthodes dans une classe que hérite d’un autre s’appelle l’enrichissement.
Ici la classe BeachBall``enrichit la classe ``Ball
avec un attribut et une méthode.
5.4.3. Redéfinition#
Le programme précédent a le défaut de ne pas afficher notre BeachBall
comme on le voudrait.
En effet, les comportement des méthodes show()
et __repr__()
ont été définis pour Ball
et non BeachBall
.
Pour obtenir le bon comportement il faudrait redéfinir ces méthodes.
Pour redéfinir une méthode, rien de plus simple, il faut simplement la définir une nouvelle fois. Ensuite le mécanisme de polymorphisme dynamique se chargera d’appeler la méthode la plus spécialisée
1#examples_inheritance.py de mon nouveau projet
2
3from example_encpsulation import Ball
4
5class BeachBall(Ball):
6
7 def __init__(self,x,y,color):
8 Ball.__init__(self,x,y)
9 self.__color=color
10
11 def get_color(self): return self.__color
12 def set_color(self, value): self.__color=value
13
14 def hide(self):
15 self.__color=0
16
17 def show(self):
18 #set color
19 c=str(self.__color%7+1)
20 sys.stdout.write("\033[3"+c+"m")
21
22 #set position
23 x=str(int(self.get_x()))
24 y=str(int(self.get_y()))
25 sys.stdout.write("u\001b["+y+";"+x+"H")
26
27 #show ball
28 sys.stdout.write("O\n")
29
30 def __repr__(self):
31 msg=Ball.__repr__(self)+" color("+self.__color+")"
32 return msg
33
34if __name__=="__main__":
35
36 bb=BeachBall(x=1,y=2,color=3)
37 bb.show()
38 Ball.show(bb)
39 bb.hide()
40 print(bb)
41 print(bb.__dict__)
Le mécanisme de polymorphisme décide quelle fonction show()
appeler à la ligne 178.
En effet ici, bb
possède 2 version de la fonction show()
: celle définie au niveau de la classe Ball
et celle définie au niveau de la classe BeachBall
.
Ce sera toujours la version la plus spécialisée qui sera appelée.
Si on souhaite appeler spécifiquement un fonction définie au niveau d’une classe mère, il est possible de le faire comme aux lignes 8, 31 et 38.
Dans la fonction __init__()
, ligne 8, l’appel au constructeur de la classe mère est systématique lorsqu’on met en œuvre un héritage.
Le terme super classe est plus académique que classe mère.
On trouve souvent l’instruction super().__init__(self)
qui permet d’appeler le constructeur de la super classe sans avoir à la nommer. Ici, on aurait aussi transmis les valeur des paramètre x
et y
: super().__init__(self, x, y)
5.4.4. Visibilité des attributs#
Revenons sur la fonction show()
de la classe BeachBall
:
1#examples_inheritance.py de mon nouveau projet
2
3from example_encpsulation import Ball
4
5class BeachBall(Ball):
6
7 ...
8
9 def show(self):
10 #set color
11 c=str(self.__color%7+1)
12 sys.stdout.write("\033[3"+c+"m")
13
14 #set position
15 x=str(int(self.get_x()))
16 y=str(int(self.get_y()))
17 sys.stdout.write("u\001b["+y+";"+x+"H")
18
19 #show ball
20 sys.stdout.write("O\n")
Rappelons nous du constructeur de la classe mère Ball
:
#module example_encapsulation.py
class Ball:
def __init__(self,x,y):
self.__x=x
self.__y=y
Dans la méthode show()
, ligne 15, nous avons écrit x=str(int(self.get_x()))
et non x=str(int(self.__x))
comme dans la version initiale de la méthode show()
de la classe Ball
. En effet, le respect de la barrière d’abstraction nous interdit de manipuler directement self.__x
en dehors de l’espace de nommage de la classe Ball
dans lequel il a été défini. Attention, cela peut paraître contre intuitif… l’object self
que nous manipulons dans le code de la classe BeachBall
possède bien un attribut x
mais il est invisible hors du code de la classe mère Ball
.
Depuis la classe fille, pour accéder à un attribut de la classe mère il faut passer par les accesseurs et mutateurs.
Indication
Un bogue très classique consiste à vouloir modifier directement un attribut de la classe mère depuis la classe fille sans que python ne détecte d’erreur. Par exemple, si nous écrivons :
1class BeachBall(Ball):
2
3 def __init__(self,x,y,color):
4 Ball.__init__(self,x,y)
5 self.__x=0.
6 self.__color=color