5.2. L’encapsulation :#

Objectifs

L’objectif de ce cours est que vous sachiez nommer et écrire dans le paradigme « objet » les notions d’encapsulation que vous connaissez déjà avec les types de données abstraits. Cette partie de cours introduit les notions de classe, d”objet, d”attribut, de méthode et d”initialisateur. A la fin du travail proposé vous saurez:

  • définir une classe simple avec des états et des comportements.

  • instancier des objets

  • accéder aux attributs d’un objet

  • invoquer des méthodes

  • détruire des instances

  • gérer la visibilité du contenu des classes.

Vous avez utilisé les types abstraits de données pour développer le projet IPI. Cette méthode n’est pas du tout imposée par le langage. C’est simplement une façon de coder et de structurer un programme dans un langage procédural. Ainsi vous vous êtes imposés l’utilisation systématique de fonctions tels que les accesseurs, les mutateurs, les constructeurs… et vous vous êtes imposés le respect de la barrière d’abstraction alors que python ne vous l’imposait pas. Vous vous êtes également efforcés de mettre dans le module d’un type toutes les fonctions qui correspondaient au fonctionnement interne de ce type. Vous avez encapsulé dans un module tout ce qui concernait un type.

Une première étape pour comprendre la notion de programmation orientée objet(POO), est d’imaginer qu’on a voulu que le langage offre une façon d’écrire qui impose l’encapsulation et le respect de la barrière d’abstraction. Ce que nous appelions jusqu’ici un type de données abstrait devient alors une classe. Les données que nous fabriquions (instancions) à partir des classes sont les objets. Il n’y a donc pas besoin de la POO pour faire de l’encapsulation, mais la POO impose l’encapsulation.

Voyons comment reformuler le type abstrait Ball en une classe Ball.

5.2.1. le type Ball#

Voici le code d’un type Ball tel que vous savez le faire.

class Ball: pass

def create(x,y):
    ball=Ball()
    ball.x=x
    ball.y=y
    return ball

def get_x(ball): return ball.x
def get_y(ball): return ball.y
def set_x(ball,value): ball.x= value
def set_y(ball,value): ball.y= value

def show(ball):
    x=str(int(ball.x))
    y=str(int(ball.y))
    sys.stdout.write("u\001b["+y+";"+x+"H"+"O")

if __name__=="__main__":
    b=create(10,3)
    set_x(b,get_x(b)+1)
    show(b)

Nous voulons transformer le type abstrait tel que codé en IPI en une classe python.

En fait nous avons déjà commencé à utiliser les classes avec l’instruction class Ball: pass. Seulement ici l’instruction ne sert qu’à déclarer un type. Étape par étape, déplaçons tout ce qui concerne le type Ball dans l”espace de nommage de la classe.

Pour suivre ce cours, il vous est conseiller de reproduire toutes les étapes présenté pour Ball sur autre type abstrait de votre choix. Reprenez éventuellement un des exemples que vous aviez mis en œuvre pour étudier les types abstraits.

5.2.2. Définition des méthodes d’une classe#

Il est possible de définir les fonctions associées au type Ball directement dans la classe. Il suffit seulement de les indenter!

#debut de la classe Ball
class Ball:

    def get_x(ball): return ball.x
    def get_y(ball): return ball.y
    def set_x(ball,value): ball.x = value
    def set_y(ball,value): ball.y = value

    def show(ball):
        x=str(int(ball.x))
        y=str(int(ball.y))
        sys.stdout.write("u\001b["+y+";"+x+"H"+"O")

#fin de la classe Ball

def create(x,y):
    ball=Ball()
    ball.x=x
    ball.y=y
    return ball

if __name__=="__main__":
    b=create(10,3)
    Ball.set_x(b,Ball.get_x(b)+1)
    Ball.show(b)

Les fonctions deviennent alors des fonctions membres de la classe Ball. On appelle aussi ces fonctions membres les méthodes de la classe Ball. Les méthodes définissent le comportement des objets de cette classe.

Les méthodes sont ici appelées en précisant le nom de leur classe : Ball.show(b) (ce qui ressemble beaucoup à la façon dont nous appelions les fonctions d’un type abstraits en dehors de son module).

Chaque fonction membre possède comme premier paramètre une référence ball vers l”objet sur lequel fonction devra agir. Une convention d’écriture du code python nous impose de toujours nommer ce premier paramètre self. Ainsi le code devient :

class Ball:

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

5.2.3. Invocations des méthodes#

Ensuite python propose une manière d’appeler les méthodes plus facile.

if __name__=="__main__":
    b=create(10,3)

    #appels en passant par la classe
    Ball.set_x(b,Ball.get_x(b)+1)
    Ball.show(b)
if __name__=="__main__":
    b=create(10,3)

    #appels en passant par l'objet
    b.set_x(b.get_x()+1)
    b.show()

Ces deux notations sont (presque) équivalentes. Python nous propose la notation de droite qui simplifie l’écriture de l’appel d’une méthode. Pour comprendre la notation de droite, imaginez que python reformule toujours objet_de_la_classe_A.m(p1,p2,p3) en A.m(objet_de_la_classe_A,p1,p2,p3)

5.2.4. Le constructeur d’une classe#

Nous n’avons pas encore rendu le constructeur create() membre de la classe. En effet python prévoit un mécanisme particulier pour décrire le constructeur d’une classe.

En fait l’instruction ball=Ball() était déjà un appel au constructeur de la classe Ball. En python le constructeur d’une classe est défini automatiquement à partir du nom de la classe ( ici c’est donc Ball() ) et invoque un initializer à l’appel du constructeur. L”initializer d’une classe se nomme toujours __init__(self). Par abus de langage, nous appellerons cette fonction le constructeur de la classe.

Voici comment on transformerait notre constructeur create() en __init__() :

class Ball: pass

def create(x,y):
    ball=Ball()
    ball.x=x
    ball.y=y
    return ball

if __name__=="__main__":
    b=create(10,3)
class Ball:

    def __init__(self,x,y):
        self.x=x
        self.y=y

if __name__=="__main__":
    b=Ball(10,3)
  • create est renommé en __init__

  • return ball est devenu le paramètre self

  • une indentation de toute la fonction permet de l’inclure dans la définition de la classe Ball

class Ball:

    def __init__(self,x,y):
        self.x=x
        self.y=y

    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.set_x(b.get_x()+1)
    b.show()

ici c’est l’instruction b=Ball(10,3) qui s’est chargée d’appeler __init__ en lui transmettant le nouvel objet self les paramètres.

5.2.5. Attributs de la classe#

Les attributs sont les données qui caractérisent l”état d’un objet. Ici, dans notre classe Ball les attributs sont x et y. L’utilisation est identique à celle du projet IPI.

Cependant, rappelez vous que lors du projet, vous vous êtes imposés le respect de la barrière d’abstraction utilisant systématiquement les accesseurs et mutateurs (get et set). Python propose un mécanisme pour forcer le respect de la barrière d’abstraction en rendant invisibles les attributs en dehors de l’espace de nommage de la classe. Pour cela, il suffit que le nom de l’attribut commence par __. Ainsi le code devient :

class Ball:

    def __init__(self,x,y):
        self.__x=x
        self.__y=y
    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.set_x(b.get_x()+1)
    b.show()

    #instruction qui provoque une erreur
    #parce qu'elle se trouve hors de l'espace de nommage de Ball
    print(b.__x)

5.2.6. Instance et classe#

Lorsque qu’on écrit ball=Ball(), on crée une instance de la classe Ball(). ball est un objet et Ball est une classe. Comprendre la distinction entre objet et classe essentiel, mais c’est exactement la même chose que la différence en entre le type Ball et la donnée ball que vous avez déjà assimilée.

5.2.7. Afficher un objet#

Que se passe t’il lorsqu’on essai d’afficher?

class Ball:

    def __init__(self,x,y):
        self.__x=x
        self.__y=y
    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(1,2)
    print(b)

On voit apparaître un message qui ressemble à cela: <__main__.Ball object at 0x100a5c790> . L’interpréteur python nous indique que b est une instance de la classe Ball déclarée dans le module principal du programme. Il nous précise également l’adresse mémoire de l’objet.

Il est possible de modifier la façon dont les objets s’affichent en écrivant la méthode spéciale __repr__() qui sera utilisée automatiquement par print() pour générer l’affichage de notre choix.

class Ball:

    def __init__(self,x,y):
        self.__x=x
        self.__y=y

    def __repr__(self):
        msg="ball("+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(1,2)
    print(b)

Vous pouvez observer le résultat en testant ce code.

5.2.8. Le Ramasse miettes#

En python, lorsque qu’il n’y a plus de référence vers une donnée, le ramasse miettes détruit automatiquement cette donnée et libérant ainsi la mémoire du programme qui avait été allouée pour cette donnée. Ce mécanisme fonctionne pour les entiers, les chaînes de caractères, les listes, les dictionnaires, etc… et pour les objets.

Lorsqu’on n’utilise plus un objet, pour le détruire, on doit effacer toutes les références du programme vers cet objet.

#creation
b = Ball()

#dereferencing
b = None

Lorsqu’un objet est détruit, le finalizer ou destructeur s’il existe est appelé automatiquement. Il se code en ajoutant la fonction __del__() dans la définition de la classe. Testez et comprenez ce code :

class Ball:

    def __init__(self,x,y):
        self.__x=x
        self.__y=y

    def __del__(self):
        print("delete:", self)

    def __repr__(self):
        msg="ball("+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__":
    print("---------start---------")

    b1=Ball(33,3)
    b2=Ball(22,2)
    b3=b2

    print("---dereferencing b1---")
    b1=None
    print("---dereferencing b2---")
    b2=None
    print("---dereferencing b3---")
    b3=None

    print("---------end---------")

    #python destroy everything when the program ends

5.2.9. Collaboration et modules#

Lorsque vous avez travaillé sur les types abstrait de données, vous avez manipulé des données qui contenaient d’autres données. Ainsi, une donnée de type Game peut connaitre des données de type Ball. Avec les classes c’est la même chose.

On peut imaginer le code suivant :

class Ball:
    #... reprendre ici le code précédent

class Game:
    def __init__(self):
        self.__ball= Ball(0,0)

    def get_ball(self): return self.__ball
    def set_ball(self,ball): self.__ball=Ball

Dans ce cas on appelle self.__ball une association et non pas un attribut.

On voit également que plusieurs classes peuvent être définies dans le même module. Si on avait voulue importer la classe Ball depuis un autre module, on aurait pu écrire from autre_module import Ball .

5.2.10. Règle d’écriture#

La PEP8 est un document qui rassemble des préconisations de style d’écriture pour le code python. Ce cours essaie de respecter ces préconisations. Vous êtes invité à faire de même lorsque vous codez en python.

5.2.11. Synthèse#

En programmation orientée objet, la déclaration d’une classe regroupe des propriétés (attributs et méthodes) communes à un ensemble d’objets. La classe définit, d’une part, des attributs représentant l’état des objets et, d’autre part, des méthodes représentant leur comportement. Une classe représente donc une catégorie d’objets. Elle apparaît aussi comme un « moule » ou une « usine » à partir de laquelle il est possible de créer des objets; c’est en quelque sorte une « boîte à outils » qui permet de fabriquer un objet. On parle alors d’un objet en tant qu’instance d’une classe (création d’un objet ayant les propriétés de la classe). (https://fr.wikipedia.org/wiki/Classe_(informatique))

Un objet ou instance se définit par :
  • son type, c’est à dire, la classe à laquelle il appartient

  • une identité qui le distingue des autres objets

  • un état :
    • on parle d”attributs, de variables membres ou de propriétés

    • lorsqu’une propriété a pour valeur une référence vers un autre objet, on parle d”assocition

  • un comportement :
    • on parle de fonctions membres, de méthodes ou de services

Pour gérer l’initialisation des instances, une classe peut fournir un constructeur (ou initializer) : la fonction __init__(). Cette fonction peut être paramétrée.

La destruction des objets déréférencés est assurée par le ramasse miette.

Pour gérer la destruction des instances, une classe peut fournir un destructeur appelé aussi finaliseur : la fonction __del__().

Toutes les fonctions membres sont définies avec un premier paramètre qui se nomme par convention self et qui prendra pour valeur à l’exécution, une référence vers l’objet effectivement manipulé.

Pour imposer la barrière d’abstraction, le nom des propriétés commencera par __. Il y aura pour chaque propriétés un accesseur et un mutateur.

#!/usr/bin/python
# -*- coding: utf-8 -*-

"""Provides MyClass class
illustrates the way python script should be structured
"""

# Built-In Imports
import os
import sys

# Third part Libs
import pygame

# Own modules
import my_other_class

#we may add properties to module
__author__ = "Gireg Desmeulles"
__license__ = "GPL"
__email__ = "desmeulles@enib.fr"
__version__ = "1.0.1"

class MyClass:
    '''comments here the class purpose'''

    #----------special methods----------

    def __init__(self, parameter_1='default value'):
        self.__attribute_1=parameter_1
        self.__association_1=object() #creation de la classe object disponible par défaut

    def __del__(self):
        print("destruction de l'instance")

    def __repr__(self):
        return("MyClass_object")

    #---------- operations----------

    def get_attribute_1(self): return self.__attribute_1
    def set_attribute_1(self, value): self.__attribute_1=value

    def get_association_1(self): return self.__association_1
    def set_association_1(self, reference): self.__association_1=reference

    def doit(self, value):
        if value < 42:
            self.attribute_1=0.

if __name__=="__main__":
    a=MyClass('value')
    a.doit(12)
    a=None

5.2.12. Exercices (à compléter)#