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

5.4.5. Exercices (à compléter)#