Fichier de rejeu Close

Indication Close

A propos de... Close

Commentaire Close

Téléchargements

Aide

Collaboration

Objectif :

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.

Dépendance

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...

Associations

Associations simples

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

Attribut ou association?
En fait il s’agit ici de définir un attribut qui a pour type une classe : 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())
Dans ce code il y a deux associations :
  • Une association de FilteredData avec Filter: self.__data_filter
  • Une association de 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

Associations multiples

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).

Associations bi-directionnelles

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.

python : code_run4.py
Sorties

            

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 :

python : code_run5.py
Sorties

            
python : code_run6.py
Sorties

            

Associations et ramasse-miettes

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"

Excercices

Labo 3:

  1. Exercices préparatoires

    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 ?

    Votre réponse :
    python : code_run9.py
    Sorties
    
                
    Une solution possible :

    Deux classes peuvent être définies : Rectangle et Point

    Rectangle aura comme propriétés :
    • un attrubut height
    • un attribut width
    • un association avec un instance de la classe Point : ``corner`
    Point aura comme propriétés :
    • x son abscisse,
    • y son ordonnée

    Le constructeur de la classe Rectangle``permettra d'instancier un ``Point pour représenter les coin supérieur gauche.

  2. Composition
    
    
    • 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)

    • Constructeur / Destructeur
      • Raffiner au constructeur de la classe Point et de la classe Rectangle les possibilités que vous jugerez utiles (valeurs par défaut ...)
      • Préciser le destructeur de la classe Point et de la classe Rectangle
      • Tester le destructeur de la classe Rectangle.
    • Objets comme valeurs de retour d’une fonction. Nous avons vu plus haut que les fonctions peuvent utiliser des objets comme paramètres. Elles peuvent également transmettre une instance comme valeur de retour.
      • Définir la fonction 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.
      • Tester cette fonction en utilisant l’objet rectangle_1 déjà défini.
    • Modifier un objet :
      • Modifier la taille d’un rectangle (sans modifier sa position), en ré-assignant ses attributs hauteur (hauteur actuelle +20) et largeur (largeur actuelle -5).
    Votre réponse :
    python : code_run13.py
    Sorties
    
                
    Une solution possible :
    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")