10  Pour quelques notions de plus

Cette partie présente quelques outils que propose Python pour une meilleur implémentation de l’Objet.

10.1 Le paramètre self de la déclaration des méthodes dans la classe

Lorsque vous écrivez une méthode, vous êtes tenus de déclarer un premier paramètre qui par convention, aura pour nom self. Ce paramètre est destiné à recevoir l’instance. Explication…

Les objets sont des instances de classes. Le principe de la programmation orientée objet consiste à définir des entités qui regroupent un état et des comportements. Il paraît évident de se représenter les objets comme des composants construits sur le modèle d’une classe et qui embarquent données (les attributs) et comportements. Mais en Python, l’implémentation de la POO est par prototype, ce n’est donc pas tout à fait ça.

Commençons par définir une classe et créons une instance.

In [1]: class Demo:
   ...:     def __init__(self, name):
   ...:         self.name = name
   ...:
   ...:     def say_hello(self):
   ...:         print(f"Hello, I'm {self.name}")
   ...: 

In [2]: d = Demo("One")

Puisque les méthodes sont des fonctions, si nous faisons appel à d.say_hello, nous devons avoir en retour une référence à la méthode (fonction) say_hello de l’objet d.

In [3]: d.say_hello
Out[3]: <bound method Demo.say_hello of <__main__.Demo object at 0x102d2ceb0>>

D’après ce retour, nous pouvons vérifier que nous pouvons accéder à Demo.say_hello.

In [4]: Demo.say_hello
Out[4]: <function __main__.Demo.say_hello(self)>

Python voit donc say_hello(self) comme une fonction définie dans la classe Demo, perçue comme un namespace.

La méthode say_hello() de l’objet (instance) d est donc une méthode liée à cette fonction.

Nous pouvons donc exécuter la fonction Demo.say_hello()… Ce qui va lever une exception  car il manque un arguments. Cet argument correspond au paramètre self.

Le paramètre self attends en effet une instance de Demo.

In [5]: Demo.say_hello(d)
Hello, I'm One

 Cette instruction, c’est ce que l’interpréteur exécute réellement. Nous écrivons une instruction qui fait appel à la méthode de l’instance et l’interpréteur va exécuter la méthode de la classe en fournissant en premier paramètre l’instance. Ce paramètre self est donc un paramètre de la méthode de la classe destiné à recevoir l’instance sur laquelle exécuter la méthode.

10.2 Notion de properties

Lorsqu’un attribut est déclaré comme non_public, pour accéder à sa valeur, li

Cette partie ne présente les properties que en tant que décorateurs

10.2.1 Accesseurs (getters)

class MyClass:

    @property
    def my_property(self):
        return 42

Usage:

>>> my_object = MyClass()
>>> my_object.my_property
42

10.2.2 Mutateurs (setters)

Le mutateur nécessite qu’un accesseur ai été défini

class MyClass:
    def __init__(self):
        self._property_value = 42

    @property
    def my_property(self):
        return self._property_value
        
    @my_property.setter
    def my_property(self, new_value):
        self._property_value = new_value

Usage:

>>> my_object = MyClass()
>>> my_object.my_property
42
>>> my_object.my_property = 24
>>> my_object.my_property
24

10.2.3 Cas pratique : notre formation

Si je reprends notre formation en ne m’intéressant qu’aux attributs, sa conception logique doit être :

Training

+String subject

-Int duration [readonly]

-List<String> students [readonly]

-Int max_seats

+Int available_seats

Note : l’outil “mermaid” utilisé pour générer ce schéma ne supporte pas la représentation standard qui devrait être -List<String> students {readonly}.

class Training:
    def __init__(self, subject:str, duration:int, seats:int):
        self.subject = subject
        self._duration = duration
        self._seats = seats
        self.students = []

Pour les attributs en lecture seule duration et students, l’ajout de properties accesseurs seules est évidente. Attention à ne pas retourner la référence vers _students.

@property
def duration(self):
    return self._duration
    
@property
def students(self):
    return self._students.copy()

Le diagramme UML indique que nous devons avoir un attribut max_seats en lecture seule représentant le nombre maximum de places. Sauf que nous avons un attribut non_public _seats et non _max_seats. Et ceci n’est pas un problème : rien n’oblige à ce que les attributs exposés (publiques) référencent un attribut privé du même nom. Donc, on ajoute la property

@property
def max_seats(self):
    return self._seats

Enfin, il y a un attribut en lecture seule available_seats. Celui-ci représente la différence entre le nombre de places de la formation et le nombre d’inscrits. Au lieu de retourner la valeur d’un attribut privé, nous pouvons calculer cette valeur :

@property
def available_seats(self):
    return self.seats - len(self._students

Et ceci illustre en quoi les properties sont un outil fondamental de l’encapsulation : elles permettent de décorréler l’implémentation de l’interface de l’objet. De l’extérieur, on voit un état cohérent pour l’utilisateur de l’objet mais l’implémentation utilise un état cohérent pour son traitement.