© Your Copyright
Reprenons l’exemple de la classe TimeSeries
du chapitre précédent.
import time
class TimeSeries:
""" contains measured data from a sensor"""
def __init__(self, time_step=1.0,unit=None,start_time=None):
self.__data=[]
self.__start_time = start_time if start_time else time.time()
self.__time_step = time_step
self.__unit = unit
def __repr__(self):
msg = "--TimeSeries--\n"
msg += " start_time= "+str(self.__start_time)
msg += " time_step= "+str(self.__time_step)
msg += " unit= "+str(self.__unit)
msg += "\n " + str(self.__data)
return msg
#----------public operations----------
def clone_empty(self):
"""build an empty clone"""
return TimeSeries(self.__time_step,self.__unit,self.__start_time)
def push_data(self, data):
self.__data.append(data)
def get_data(self):
return tuple(self.__data)
Dans ce chapitre, nous souhaitons étendre le code précédent pour y ajouter de nouvelles méthodes ou de nouveaux attributs.
Cependant, le code du chapitre précédent est finalisé et nous ne souhaitons pas le modifier.
Comment alors réutiliser ce code existant pour le faire évoluer?
La première idée qui nous vient à l’esprit est de recopier le module time_series.py
.
Ce qui est dommage alors, est que si nous découvrons et corrigeons un bogue dans time_series.py
en faisant évoluer le code du chapitre précédent, il nous faudra penser à propager le correctif dans chacune des 2 copies du code.
Si il n’y a que 2 copies, c’est déjà une source importante d’oublis.
Mais si le nombre de copies augmente à chaque chapitre, cela deviendra vraiment 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.
L’héritage est le mécanisme qui permet l’extension des classes.
Pour étendre la classe TimeSeries
nous créons une nouvelle classe ExtendedTimeSeries
en indiquant au moyen de parenthèses, la classe qu’elle étend.
from time_series import TimeSeries
class ExtendedTimeSeries(TimeSeries):
""" ExtendedTimeSeries inherits from TimeSeries"""
pass
ets = ExtendedTimeSeries()
ets.push_data(10)
ets.push_data(11)
ets.push_data(12)
Pour l’instant nous avons défini ExtendedTimeSeries
comme une classe vide qui hérite de TimeSeries
.
ExtendedTimeSeries
récupère alors tout le contenu de TimeSeries
. C’est pourquoi il est possible d’appeler la méthodes push_data()
alors qu’il n’y a aucune mention de push_data
dans ExtendedTimeSeries
.
TimeSeries
est appelée classe mère ou superclasse de la classe ExtendedTimeSeries
.
ExtendedTimeSeries
est appelée classe fille, classe dérivée ou spécialisation de la classe TimeSeries
Ainsi une classe hérite des propriétés de superclasse, elle même héritant des propriétés de sa superclasse, etc... L’héritage est transitif, Si A
hérite de B``et ``B``hérite de ``C
, alors A
hérite de C
.
En python, lorsqu’on défini une nouvelle classe en ne spécifiant aucune super classe, la nouvelle classe hérite par défaut de la classe object
.
Finalement, toute classe hérite de la classe objet directement ou par transitivité.
Voici 3 notations équivalentes:
class MyClass :
pass
class MyClass() :
pass
class MyClass(object) :
pass
Il est possible d’enrichir la classe fille en y ajoutant des méthodes et des attributs qui n’étaient pas présents dans la classe mère.
Ici, nous ajoutons un attribut sensor
en lecture seule et une méthode publique compute_meam_value
qui renvoie la valeur moyenne des mesures.
from time_series import TimeSeries
class ExtendedTimeSeries(TimeSeries):
""" ExtendedTimeSeries inherits from TimeSeries"""
def __init__(self,time_step=1.0,unit=None,start_time=None,sensor=""):
TimeSeries.__init__(self,time_step,unit,start_time)
self.__sensor = sensor
#----------public operations----------
def get_sensor(self):
return self.__sensor
def compute_meam_value(self):
sum = 0.
for v in self.get_data():
sum += v
return sum/len(self.get_data())
ets = ExtendedTimeSeries(time_step=0.1,sensor="sonar",unit="mm")
ets.push_data(10)
ets.push_data(11)
ets.push_data(12)
print(ets.compute_meam_value())
Remarquons comment nous avons appelé le constructeur de TimeSeries
de la classe mère, depuis le constructeur de la classe fille.
TimeSeries.__init__(self,time_step,unit,start_time)
L’enrichissement nous permet d’ajouter des propriétés au code étendu, mais comment modifier des propriétés déjà présentes dans la classe mère?
Par exemple, nous souhaitons faire en sorte que les données stockées dans un ExtendedTimeSeries
soient définies avec un type réel.
Pour cela il faut redéfinir (override) la méthode push_data()
.
from time_series import TimeSeries
class ExtendedTimeSeries(TimeSeries):
""" ExtendedTimeSeries inherits from TimeSeries"""
def __init__(self, time_step=1.0, unit=None, start_time=None, sensor=""):
TimeSeries.__init__(self,time_step,unit,start_time)
self.__sensor = sensor
#----------public operations----------
def get_sensor(self):
return self.__sensor
def compute_meam_value(self):
sum = 0.
for v in self.get_data():
sum += v
return sum/len(self.get_data())
def push_data(self, data):
"""override TimeSeries.push_data"""
value = float(data)
super().push_data(value)
ets = ExtendedTimeSeries(time_step=0.1,sensor="sonar",unit="mm")
ets.push_data(10)
ets.push_data(11)
ets.push_data(12)
print(ets.compute_meam_value())
La méthode redéfinie dans la classe fille remplace alors la méthode précédemment définie dans la classe mère.
Il existe des méthodes spéciales qui peuvent être redéfinies pour permettre à une classe de se comporter comme d’autres types python. On peut par exemple redéfinir l’opérateur d’addition pour émuler un type arithmétique :
Le lien suivant décrit toutes les fonctions spéciales qui peuvent être redéfinies pour un object
en python : https://docs.python.org/3/reference/datamodel.html
Dans notre exemple, que se passe-t’il si on appelle la méthode ets.clone_empty()
? Nous obtiendrons une instance de TimeSeries``et non d'``ExtendedTimeSeries
, puisque c’est bien le constructeur de ``TimeSeries``qui sera appelé.
Il faut alors redéfinir la méthode clone pour obtenir le bon résultat.
from time_series import TimeSeries
class ExtendedTimeSeries(TimeSeries):
""" ExtendedTimeSeries inherits from TimeSeries"""
def __init__(self,time_step=1.0,unit=None,start_time=None,sensor=""):
TimeSeries.__init__(self,time_step,unit,start_time)
self.__sensor = sensor
#----------public operations----------
def get_sensor(self):
return self.__sensor
def compute_meam_value(self):
sum = 0.
for v in self.get_data():
sum += v
return v/len(self.get_data())
def clone_empty(self):
"""build an empty clone"""
#ERROR!!!
return ExtendedTimeSeries(self.__time_step,self.__unit,self.__start_time)
Le problème ici est que self.__time_step, self.__unit, self.__start_time
ne sont visibles que depuis le code membre de la classe TimeSeries
et non ExtendedTimesSeries
.
Il existe en programmation orientée objet un type de visibilité entre publique et privée. C’est la visibilité protégée. Une propriété protégée (attribut ou méthode) n’est visible que depuis l’intérieur d’une classe et depuis l’intérieur de ses classes filles.
Python ne propose pas de visibilité protégée.
Une pratique répandue est de signaler l’intention de rendre une propriété protégée en préfixant son nom d’un “_
”. Par exemple, self.__sensor
deviendrait self._sensor
.
En pratique, comme pour les attributs privés, nous coderons les attributs protégés comme des attributs privés avec accesseurs et mutateurs protégés :
import time
class TimeSeries:
""" contains measured data from a sensor"""
def __init__(self, time_step=1.0,unit=None,start_time=None):
self.__data=[]
self.__start_time = start_time if start_time else time.time()
self.__time_step = time_step
self.__unit = unit
#----------public operations----------
def clone_empty(self):
"""build an empty clone"""
return TimeSeries(self.__time_step,self.__unit,self.__start_time)
def push_data(self, data):
self.__data.append(data)
def get_data(self):
return tuple(self.__data)
#---------protected operations---------
def _get_start_time(self):
return self.__start_time
def _get_time_step(self):
return self.__time_step
def _get_unit(self):
return self.__unit
from time_series import TimeSeries
class ExtendedTimeSeries(TimeSeries):
""" ExtendedTimeSeries inherits from TimeSeries"""
def __init__(self,time_step=1.0,unit=None,start_time=None,sensor=""):
TimeSeries.__init__(self,time_step,unit,start_time)
self.__sensor = sensor
#----------public operations----------
def get_sensor(self):
return self.__sensor
def compute_meam_value(self):
sum = 0.
for v in self.get_data():
sum += v
return v/len(self.get_data())
def clone_empty(self):
"""build an empty clone"""
return ExtendedTimeSeries(self._get_time_step(),self._get_unit(),self._get_start_time())
Remarquons ici que le fait d’étendre la classe TimeSeries
en utilisant la visibilité protégée nous a obligé à modifier son code en ajoutant des accesseurs. Le principe d’ouverture/fermeture n’est pas complètement respecté dans ce cas. Il est conseillé de limiter l’utilisation de la visibilité protégée si l’extension de code n’a pas été prévue et si c’est possible.
Un objet est du type de sa classe et est aussi du type de toutes les super classes de sa classe :
La fonction python isinstance(object_ref, class_ref)
teste si un objet est d’un certain type.
Lorsque qu’une méthode a été redéfinie par la classe ou par les superclasses d’un objet, plusieurs “forme” de cette méthode sont alors accessibles. Le polymorphisme par sous typage est le mécanisme qui permet de déterminer quelle “forme” d’une méthode doit être exécutée : la “forme” la plus spécialisée.
Par exemple :
Cet exemple illustre le fait qu’on ne connaîtra le type précis des instances qu’à l’exécution. On parle alors de polymorphisme dynamique.
En effet, la liste list_of_a
, est vue comme un conteneur d’instances de la classe “A”; parmi elles, certaines A sont aussi des instances de B ou de C.
C’est à l’exécution, lors de l’évaluation de l’expression a.do_it()
, que l’interpréteur python recherchera la forme (-morphe) la plus spécialisée de do_it()
dans l’arbre d’héritage de la classe de l’instance considérée.
Le polymorphisme dynamique est une propriété fondamentale du paradigme de programmation orientée objet. Malheureusement, cette propriété a un coût. En effet, dès lors qu’une méthode peut prendre plusieurs formes à l’exécution, il convient de se demander à chaque appelle de méthode sur un objet, où se trouve le code de cette méthode. Cela introduit des indirections dans le code exécuté et donc une perte de performances.
Notons, que parmi les critiques du paradigme objet, on entend que la plupart du temps un polymorphisme statique (qui se détermine à la compilation) aurait suffit et que donc le coût en performance du polymorphisme dynamique ne se justifie pas. Ici, avec le langage python qui est un langage interprété de toute façon très dynamique, la question se pose beaucoup moins (Il n’y a pas d’étape de compilation, et la manipula)
L’héritage multiple est la possibilité pour une classe d’hériter de plusieurs classes à la fois. Voici un exemple d’une voiture hybride qui hériterait d’une voiture électrique et d’une voiture à essence.
L’héritage multiple vous est présenté car il s’agit d’une possibilité présente dans certains langage comme python. Mais nous n’insisterons pas sur l’héritage multiple parce qu’il est possible de s’en passer en utilisant d’autres mécanismes. Mais surtout parce qu’il pose de nombreux problèmes.
Ici le problème dit du “diamant”:
Difficile de déterminer quel méthode sera appelée lors de l’appel hybrid_car.start()
. Celle de GasolineCar
ou de ElectricCar
?
Mais surtout, on voit à l’exécution que le constructeur de Car
a été appelé 2 fois là ou une seule voiture a été instanciée! Cette redondance peut être compliquée à gérer correctement.
Contrairement à l’héritage multiple, la notion d’abstraction est une notion importante. Elle intervient lorsque l’on pense l’héritage, non en terme de spécialisation, mais en terme de généralisation.
Pour illustrer sur un exemple simpliste, imaginons que nous avons 3 classes :
import random
class Goblin :
def __init__(self):
self.__health = 10
def looseHealth(self,amount):
self.__health = self.__health - amount
def isDead(self):
return True if self.__health else False
def attack(self, enemy):
enemy.looseHealth(3)
class Player :
def __init__(self):
self.__health = 30
self.__strenght = 20
def looseHealth(self,amount):
self.__health = self.__health - amount
def isDead(self):
return True if self.__health else False
def attack(self, enemy):
enemy.looseHealth(self.__strenght*random.random())
class Troll :
def __init__(self):
self.__health = 100
def looseHealth(self,amount):
self.__health = self.__health - amount
def isDead(self):
return True if self.__health else False
def attack(self, enemy):
enemy.looseHealth(random.randint(0,self.__health)
Il est possible de factoriser le code de ces 3 classes en proposant une super classe.
import random
class Animat :
"""Defines abstract animat"""
def __init__(self,health):
self._health = health
def looseHealth(self,amount):
self._health = self._health - amount
def isDead(self):
return True if self._health else False
def attack(self, enemy):
"""abstract method"""
raise NotImplementedError("Subclasses should implement attack()!")
class Goblin(Animat) :
"""Defines Goblin concrete class from Animat"""
def __init__(self):
Animat.__init(self,10)
def attack(self, enemy):
enemy.looseHealth(3)
class Player(Animat) :
"""Defines Player concrete class from Animat"""
def __init__(self):
Animat.__init(self,30)
self.__strenght = 20
def attack(self, enemy):
enemy.looseHealth(self.__strenght*random.random())
class Troll(Animat) :
"""Defines Troll concrete class from Animat"""
def __init__(self):
Animat.__init(self,100)
def attack(self, enemy):
enemy.looseHealth(random.randint(0,self._health)
Ici Animat
est une classe dite abstraite. Elle n’a pas vocation à être instanciée en tant que t’elle. Seul ses classes filles auront un sens concret.
On reconnaît une classe abstraite au fait qu’elle possède au moins une méthode abstraite.
Une méthode abstraite est une méthode qui est déclarée mais doit absolument être redéfinie dans les classes concrètes plus spécialisées. En python, on reconnaît une méthode abstraite à l’instruction qui génère une levée d’exception raise NotImplementedError()
.
A FAIRE
voir poly