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ètreself
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