© Your Copyright
Ce chapitre a pour objet de présenter les notions de dépendance et d’association. Ces notions permettent la collaboration entre classes pour réaliser des tâches complexes.
En programmation orientée objet, il est préconisé d’adopter le principe de responsabilité unique. Une classe doit avoir une et une seule responsabilité. Par exemple, on fait en sorte qu’une classe ne représente qu’une seule chose bien identifiée lors de la conception. Ainsi, pour accomplir des tâches complexes, les classes collaborent les unes avec les autres. Leurs instances peuvent s’envoyer des messages, se connaître, se composer, etc... La collaboration entre objets et donc entre classe est un mécanisme essentiel de la POO.
Les deux mécanismes de collaboration présentés ici sont la dépendance et l’association.
Prenons un exemple pour introduire la notion de dépendance entre classes.
Tout d’abord classe TimeSeries
qui permet d’instancier des séries temporelles pour faire par exemple des relevés de mesures.
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)
Une deuxième classe Filter
permet de fabriquer une nouvelle série temporelle en appliquant une fonction mathématique sur chacune des mesures.
from time_series import TimeSeries
class Filter:
""" filter can modify data from time_series """
def __init__(self, filter_function):
self.__filter_function=filter_function
#----------public operations----------
@staticmethod
def cut_negative(value):
"""cut negative value"""
return value if (value>0.) else 0.
@staticmethod
def abs(value):
"""return absolute value"""
return abs(value)
@staticmethod
def square(value):
"""reurn square value"""
return value*value
def filter(self, time_series):
assert isinstance(time_series,TimeSeries)
out_time_series = time_series.clone_empty()
for value in time_series.get_data():
out_time_series.push_data(self.__filter_function(value))
return out_time_series
if __name__ == '__main__':
ts=TimeSeries(0.1,"mv")
ts.push_data(100.)
ts.push_data(50.)
ts.push_data(-50.)
ts.push_data(50.)
print(ts)
f=Filter(Filter.cut_negative)
print(f.filter(ts))
On remarquera que la méthode filter(self, time_series)
attend en paramètre un objet de type TimeSeries
et propose une valeur de retour de même type.
La fonction isinstance()
permet de s’assurer du type de time_series
.
On notera également en entête du fichier : from time_series import TimeSeries
. Il y a une dépendance de la classe Filter
vers la classe TimeSeries
.
Cette notion de dépendance est importante pour assurer la modularité du code. Il est intéressant de limiter les dépendances inutiles, surtout les inter-dépendances. On remarquera dans l’exemple que Filter``dépend de ``TimeSeries
mais pas l’inverse.
Enfin, n’oublions qu’il ne s’agit ici que d’un exemple illustratif et que python fournit déjà une fonction native filter
qui utilise des fonctions lambda
pour faire ce genre de traitement...
Nous avons illustré la dépendance avec le cas d’une classe qui utilisait le code d’une autre classe. Pour aller plus loin, on peut imaginer qu’un objet conserve une référence vers un autre objet dont il a besoin pour fonctionner.
Si un objet d’une classe ``A`` possède une référence vers un objet de la classe ``B``, il y a une association de A vers B
self.__role=MyClass()
.
Pour python il n’y a aucune différence.
Mais dans le paradigme de la programmation objet, indépendamment du langage utilisé, nous distinguons les attributs qui sont un des 5 types de bases (integer
, string
, real
, boolean
, char
) des associations qui sont des références vers des objets.
Cette distinction attribut/association devient importante lorsqu’on s’intéresse à la modélisation UML.
Comme il n’y a aucune différence avec les attributs, nous codons les associations de la même manière.
from time_series import TimeSeries
from filter import Filter
class FilteredData():
""" FilteredData associate TimeSeries and Filter instances"""
def __init__(self,time_series=TimeSeries(),data_filter=None):
assert isinstance(time_series,TimeSeries)
self.__input_time_series = time_series
assert isinstance(data_filter,Filter)
self.__data_filter = data_filter
#----------public operations----------
def get_input_time_series(self):
return self.__input_time_series
def set_input_time_series(self,input_time_series):
assert isinstance(input_time_series,TimeSeries)
self.__input_time_series=input_time_series
def get_data_filter(self):
return self.__data_filter
def set_data_filter(self,data_filter):
assert isinstance(data_filter,Filter)
self.__data_filter=data_filter
input_time_series = property(get_input_time_series, set_input_time_series)
data_filter = property(get_data_filter,set_data_filter)
def compute_output_time_series(self):
return self.__data_filter.filter(self.__input_time_series)
if __name__ == '__main__':
#build time_series
ts=TimeSeries(0.1,"mv")
ts.push_data(100.)
ts.push_data(50.)
ts.push_data(-50.)
ts.push_data(50.)
#build 2 filter
f1=Filter(Filter.abs)
f2=Filter(Filter.square)
#build filtered data with filter 1
fd=FilteredData(ts,f1)
print(fd.input_time_series)
print(fd.compute_output_time_series())
#change filter
fd.data_filter=f2
#idem fd.set_data_filter(f2)
print(fd.compute_output_time_series())
FilteredData
avec Filter
: self.__data_filter
FilteredData
avec TimeSeries
: self.__input_time_series
Nous noterons que l’association est directionnelle. Si un objet FilteredData
référence bien un objet TimeSeries
, un objet TimeSeries
, lui, ne référence pas d’objet FilteredData
.
Implicitement, si FilteredData
est associé avec TimeSeries
, il y a une dépendance de FilteredData
vers TimeSeries
Maintenant, si nous souhaitons appliquer plusieurs filtres sur nos données, alors il faudrait qu’un objet FilteredData
soit associé à plusieurs instances de Filter
. Dans ce cas il suffit regrouper les références dans une liste.
from time_series import TimeSeries
from filter import Filter
class FilteredData():
""" FilteredData associate TimeSeries and Filter instances"""
def __init__(self,time_series=TimeSeries()):
assert isinstance(time_series,TimeSeries)
self.__input_time_series = time_series
self.__data_filters = []
#----------public operations----------
def get_input_time_series(self):
return self.__input_time_series
def set_input_time_series(self,input_time_series):
assert isinstance(input_time_series,TimeSeries)
self.__input_time_series=input_time_series
input_time_series = property(get_input_time_series, set_input_time_series)
def add_data_filter(self,data_filter):
assert isinstance(data_filter,Filter)
self.__data_filters.append(data_filter)
def get_data_filters(self):
return tuple(self.__data_filters)
def reset_data_filters(self):
self.__data_filters.clear()
def compute_output_time_series(self):
input_time_series = self.__input_time_series
output_time_series = self.__input_time_series
#apply each filter, order is important
for f in self.__data_filters :
output_time_series = f.filter(input_time_series)
input_time_series = output_time_series
return output_time_series
if __name__ == '__main__':
#build time_series
ts=TimeSeries(0.1,"mv")
ts.push_data(100.)
ts.push_data(50.)
ts.push_data(-50.)
ts.push_data(50.)
#build 2 filters
f1=Filter(Filter.abs)
f2=Filter(Filter.square)
#build filtered data
fd=FilteredData(ts)
fd.add_data_filter(f1)
fd.add_data_filter(f2)
print(fd.input_time_series)
print(fd.compute_output_time_series())
fd.reset_data_filters()
Dans ce cas on parle d’une association avec une multiplicité.
En général, comme pour les attributs avec multiplicité, cela conduit à proposer des méthodes pour gérer cette association différentes des accesseurs/mutateurs.
Ici nous avons proposé les services add_data_filter
, get_data_filters
et reset_data_filters
.
Pour ne pas rompre le principe d’encapsulation, l’accesseur : get_data_filters
renvoie une copie de la liste de filtres avec l’instruction return tuple(self.__data_filters)
.
Il peut arriver que deux objets se référencens mutuellement.
Prenons par exemple une classe Eleve
. Chaque instance de cette classe seraient associée à un binôme. Il est nécessaire que ce référencement soit mutuel : chacun des membres du binôme connaît l’autre membre.
Cela fonctionne mais c’est risqué! En effet, en tant qu’utilisateur de la classe, je dois m’assurer que jacques``et ``romuald
se connaissent mutuellement pour ne pas introduire d’incohérence. Si “Jacques” est en binôme avec “Romuald” mais que Romuald l’ignore, cela peut poser problème.
Une solution consiste à supprimer le mutateur set_partner
et proposer un service make_team_with()
qui assure un invariant de référencement mutuel :
Les associations ne sont pas anodine du point de vue du garbage collector. L’exemple suivante montre comment une association (ici un auto référencement) peut neutraliser l’action du garbage collector. Attention, cette exemple ne fonctionne plus depuis le passage à python 3. Seul un test avec python 2 exhibera un comportement approprié.
class Immortal :
""" class Immortal can't be destroyed by garbage collector """
def __init__ (self):
self.__self=self
print('new Immortal')
def __del__(self):
print('del Immortal')
i =Immortal()
i= None
print "progam ending"
Voici, un autre exemple poétique, avec des fées et des enfants! Cette fois il y a une association directionnelle multiple. Tant qu’il reste un enfant pour croire en une fée, elle ne peut pas mourir :
class Fairy :
"""Fairy can't be destroyed if some child still believe in it"""
def __init__ (self, name):
self.__name=name
msg = self.__name + " fairy birth"
print(msg)
def __del__(self):
msg = self.__name + " die"
print(msg)
def get_name(self):
return self.__name
class Child :
""" class Immortal can't be destroyed by garbage collector """
def __init__ (self):
self.__self=self
self.__faiths=[]
def faith_in(self,fairy):
assert isinstance(fairy,Fairy)
#invariant : no duplicate
if not fairy in self.__faiths :
self.__faiths.append(fairy)
def forget(self):
if len(self.__faiths):
self.__faiths.pop()
tinker_bell = Fairy("Tinker Bell")
blue_fairy = Fairy("Blue")
romuald = Child()
alexis = Child()
romuald.faith_in(tinker_bell)
romuald.faith_in(blue_fairy)
alexis.faith_in(blue_fairy)
tinker_bell = None
blue_fairy = None
print("Alexis forget last : Blue")
alexis.forget()
print("Romuald forget last : Blue")
romuald.forget()
print("Romuald forget last : Tinker Bell")
romuald.forget()
print "progam ending"
Pour définir un rectangle, nous spécifions sa largeur, sa hauteur et précisons la position du coin supérieur gauche. Une position est définie par un point (coordonnées x et y). Quelle(s) classe(s) et quel(s) attribut(s) peuvent être définis pour représenter ces notions ? A quoi pourra servir le constructeur de chaque classe ?
Deux classes peuvent être définies : Rectangle
et Point
Rectangle
aura comme propriétés :Point
: ``corner`Point
aura comme propriétés :x
son abscisse,y
son ordonnéeLe constructeur de la classe Rectangle``permettra d'instancier un ``Point
pour représenter les coin supérieur gauche.
Définir la classe Point contenant les attributs x et y (coordonnées)
Définir la classe Rectangle
Instancier un objet rectangle_1
de largeur 50, de hauteur 35, et dont le coin supérieur gauche se situe au coordonnée (12,27)
find_center()
qui est appelée avec un argument de type Rectangle et qui renvoie un objet Point, lequel contiendra les coordonnées du centre du rectangle.rectangle_1
déjà défini.class Point:
"""point defines a 2D point"""
def __init__(self,x=0.,y=0.):
self.__x=x
self.__y=y
def __del__(self):
print("del point")
def __repr__(self):
msg = "Point : " + str(self.__x) + ", " + str(self.__y)
return msg
#------public methods------
def get_x(self):
return self.__x
def set_x(self,x):
self.__x=x
def get_y(self):
return self.__y
def set_y(self,y):
self.__y=y
class Rectangle:
"""defines rectangle with up left corner(Point instance), width(float) an height(float)"""
def __init__(self, width=1., height=1., corner_x=0., corner_y=0.):
self.__width = width
self.__height = height
self.__corner = Point(corner_x,corner_y)
def __del__(self):
print("del rectangle")
#------public methods------
def get_width(self):
return self.__width
def get_height(self):
return self.__height
def get_corner(self):
return self.__corner
def set_width(self,width):
if width >=0 :
self.__width = width
else :
self.__corner.set_x(self.__corner.get_x()+width)
self.__width = -width
def set_height(self,height):
if height >=0 :
self.__height = width
else :
self.__corner.set_y(self.__corner.get_y()+height)
self.__height = -height
def find_center(rectangle):
corner = rectangle.get_corner()
x = corner.get_x() + rectangle.get_width()/2.0
y = corner.get_y() + rectangle.get_height()/2.0
return Point(x,y)
if __name__== "__main__":
rectangle_1 = Rectangle(50,35,12,27)
print(find_center(rectangle_1))
rectangle_1.set_width(20)
rectangle_1.set_height(-5)
rectangle_1 = None
print("end")