L'encapsulation : ================= .. admonition:: 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``. le type ``Ball`` ------------------ Voici le code d'un type ``Ball`` tel que vous savez le faire. .. code-block:: python 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. 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! `_ .. code-block:: python #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 : .. code-block:: python 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") Invocations des méthodes ------------------------ Ensuite python propose une manière d'appeler les méthodes plus facile. +-----------------------------------------+--------------------------------------+ | | | |.. code-block:: Python |.. code-block:: Python | | | | | if __name__=="__main__": | if __name__=="__main__": | | b=create(10,3) | b=create(10,3) | | | | | #appels en passant par la classe | #appels en passant par l'objet| | Ball.set_x(b,Ball.get_x(b)+1) | b.set_x(b.get_x()+1) | | Ball.show(b) | 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)`` 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__()`` : +----------------------------------------+-------------------------------+ | | | |.. code-block:: Python |.. code-block:: Python | | | | | class Ball: pass | class Ball: | | | | | def create(x,y): | def __init__(self,x,y):| | ball=Ball() | self.x=x | | ball.x=x | self.y=y | | ball.y=y | | | return ball | if __name__=="__main__": | | | b=Ball(10,3) | | if __name__=="__main__": | | | b=create(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`` .. code-block:: Python 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. 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 : .. code-block:: Python 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) **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. Afficher un objet ----------------- Que se passe t'il lorsqu'on essai d'afficher? .. code-block:: Python 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. .. code-block:: Python 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. 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. .. code-block:: python #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 : .. code-block:: Python 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 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 : .. code-block:: Python 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`` . 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. 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. .. code-block:: #!/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 Exercices (à compléter) ----------------------- .. include:: exercices/encapsulation_ex.rst