heritage
========
.. admonition:: 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**.
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.
.. code-block:: Python
#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()
.. code-block:: Python
#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 :
.. code-block:: Python
print(isinstance(Ball,bb))
print(isinstance(BeachBall,bb))
print(isinstance(Ball,b))
print(isinstance(BeachBall,b))
Voyons maintenant comment ajouter des propriétés (enrichissement) dans ce code étendu.
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 :
.. code-block:: Python
#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.
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**
.. code-block:: Python
:linenos:
#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
def show(self):
#set color
c=str(self.__color%7+1)
sys.stdout.write("\033[3"+c+"m")
#set position
x=str(int(self.get_x()))
y=str(int(self.get_y()))
sys.stdout.write("u\001b["+y+";"+x+"H")
#show ball
sys.stdout.write("O\n")
def __repr__(self):
msg=Ball.__repr__(self)+" color("+self.__color+")"
return msg
if __name__=="__main__":
bb=BeachBall(x=1,y=2,color=3)
bb.show()
Ball.show(bb)
bb.hide()
print(bb)
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)``
Visibilité des attributs
------------------------
Revenons sur la fonction ``show()`` de la classe ``BeachBall``:
.. code-block:: Python
:linenos:
#examples_inheritance.py de mon nouveau projet
from example_encpsulation import Ball
class BeachBall(Ball):
...
def show(self):
#set color
c=str(self.__color%7+1)
sys.stdout.write("\033[3"+c+"m")
#set position
x=str(int(self.get_x()))
y=str(int(self.get_y()))
sys.stdout.write("u\001b["+y+";"+x+"H")
#show ball
sys.stdout.write("O\n")
Rappelons nous du constructeur de la classe mère ``Ball``:
.. code-block:: Python
#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.
.. HINT::
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 :
.. code-block:: Python
:linenos:
class BeachBall(Ball):
def __init__(self,x,y,color):
Ball.__init__(self,x,y)
self.__x=0.
self.__color=color
Exercices (à compléter)
-----------------------
.. topic:: Casse Briques
.. raw:: html
Énoncé
Reprendre le code objet du casse brique : :download:`code <../_static/code/casse_briques_poo.zip>`
#. Ajouter une classe ``EasyLevel`` dans le module ``level.py``
#. Redéfinir la méthode ``touch_brick()`` pour qu'il suffise de ne toucher qu'une seule fois une brique pour la détruire.
#. Tester la classe ``EasyLevel`` dans le module ``level.py``
#. Import la classe ``EasyLevel`` dans le module ``main.py``
#. Dans la fonction ``init()`` faire en sorte que le premier niveau soit un ``EasyLevel`` plutôt qu'un ``Level``
.. raw:: html
.. raw:: html
Solution
:download:`code <../_static/code/casse_briques_poo_heritage.zip>`
.. raw:: html