Autres améliorations

Nous montrons ici quelques améliorations parfois utiles pour nos classes. Il n'est pas fondamental de les connaître lorsque l'on débute, mais deviennent (très) utiles lorsque l'on commence à prendre de la bouteille.

Sérialisation des objets

La façon la plus classique de représenter un objet sous la forme d'un texte est, on l'a vu, d'utiliser la fonction str() en combinaison avec la méthode spéciale __str__. Cette représentation textuelle a pour but d'être affichée à l'écran.

Il existe une autre représentation textuelle, appelée sérialisation :

Définition

La sérialisation d'un objet est sa transformation sous une forme textuelle permettant de le reconstituer.

Par exemple la chaîne de caractère "42" permet de reconstruire un objet de type entier valant $42$, on encore "[1, 2, 3, 4]" permet de reconstruire une liste d'entiers.

L'appel à la sériation se fait en python en utilisant la fonction repr() :

>>> repr(1)
'1'
>>> repr([1, 2, 3, 4])
'[1, 2, 3, 4]'

Une forme classique de sérialisation consiste à écrire la chaîne de caractère qui permettrait de le créer, c'est à dire une chaîne de caractère permettant de reconstituer ses attributs. En programmation objet cela la signifie souvent écrire l'objet sous la forme d'un appel à un constructeur, si celui ci prend tous les paramètres en paramètre. Par exemple pour notre compteur dont le constructeur est :

class Compteur:
    def __init__(self, pas=1, valeur=0):
        self.pas = pas
        self.valeur = valeur

    # ...

La sérialisation, appelée via la fonction spéciale __str__ consisterait en :

class Compteur
    # ...
    def __repr__(self):
        return "Compteur(pas=" + str(self.pas) + ", valeur=" + str(self.valeur) + ")"

On a alors

c = Compteur(pas=12)
c.incrémente()
c.incrémente()
print(repr(c))

va afficher :

Compteur(pas=12, valeur=24)

Remarquez la différence avec str qui est définie comme :

class Carte:
    # ...

    def __str__(self):
        return "Le compteur vaut " + str(self.valeur)
    
    # ...

Et qui ne permet pas de reconstituer le compteur (il manque son pas) :

c = Compteur(pas=12)
c.incrémente()
c.incrémente()
print(repr(c))
print(c)

va afficher :

Compteur(pas=12, valeur=24)
Le compteur vaut 24

Il est toujours utile d'avoir une sérialisation de ses objets car on peut l'utiliser aussi pour l'afficher.

À retenir

En première approche, codez une méthode __repr__ pour toutes vos classes car par défaut si l'on ne définit par __str__, c'est __repr__ qui est utilisé (le __str__ de la classe object rend le résultat de son __repr__).

Utiliser des méthodes comme des attributs

Pour notre compteur, on peut accéder directement aux attributs valeur et pas. On pourrait avoir envie de ne tolérer que des pas non nul mais pour l'instant rien ne nous empêche d'écrire :

c = Compteur()
c.pas = 0

Et de créer un compteur qui n'incrémente jamais...

Pour éviter cela, on a vu que l'on pouvait rendre l'attribut privé et y accéder via des accesseurs :

class Compteur:
    def __init__(self, pas=1, valeur=0):
        assert pas != 0
        self._pas = pas

        self.valeur = valeur

    # ...

    def get_pas(self):
        return self._pas

    def set_pas(self, pas):
        assert pas != 0
        self._pas = pas

Le code précédent alourdi le code et force l'utilisation de méthodes alors que c'est bien un attribut que l'on modifie. Python a une superbe fonctionnalité qui permet d'utiliser les accesseurs les mutateurs comme si l'on utilisait directement un attribut ! Pour cela on utilise la classe property de python, qui s'écrit comme une variable de classe :

class Compteur:
    def __init__(self, pas=1, valeur=0):
        assert pas != 0
        self._pas = pas

        self.valeur = valeur

    # ...

    def _get_pas(self):
        return self._pas

    def _set_pas(self, pas):
        assert pas != 0
        self._pas = pas

    pas = property(_get_pas, _set_pas)

On a rendu les accesseurs privé et peut les utiliser maintenant comme un attribut :

On peut maintenant écrire le code suivant :

c = Compteur()
c.pas = 12

Qui utilisera le mutateur en sous-main. Si on ne veut pas permettre de modifier un attribut, il suffit de ne coder que l'accesseur et non le mutateur, par exemple si on ne veut veut pas laisser l'utilisateur modifier la valeur mais qu'on veut qu'il puisse la consulter on peut :

class Compteur:
    def __init__(self, pas=1, valeur=0):
        assert pas != 0
        self._pas = pas

        self._valeur = valeur

    # ...

    def _get_valeur(self):
        return self._valeur

    valeur = property(_get_valeur)

Affecter une valeur produira une erreur :

>>> c = Compteur()
>>> print(c.valeur)
0
>>> c.valeur = 12
Traceback (most recent call last):
  File "<python-input-27>", line 1, in <module>
    c.valeur = 12
    ^^^^^^^^
AttributeError: property 'valeur' of 'Compteur' object has no setter

Méthodes de classes

On a déjà vu les attributs de classes, on peut faire pareil avec les méthodes.

Définition

Une méthode de classe est une méthode dont le premier paramètre est la classe de l'objet et non l'objet en lui même.

On peut les utiliser via la notation pointée avec la classe à gauche du point ou l'objet.

Le principal intérêt des méthodes de classes et de permettre des créations alternatives des objets. Par exemple pour notre compteur on pourrait avoir la méthode de classe suivante, définie par un décorateur @classmethod placé juste au-dessus de sa définition :

class Compteur:
    # ...

    @classmethod
    depuis_compteur(cls, compteur):
        return cls(compteur.pas, compteur.valeur)

    # ...

Cette fonction nous permet d'écrire le code suivant qui crée des objets de type compteur en utilisant comme paramètre un autre compteur :


c = Compteur()
c.incrémente()

c_copie = Compteur.depuis_compteur(c)

La ligne @classmethod est ce qu'on appelle un décorateur. Python en utilise un peut partout pour spécifier l'utilisation des fonctions décorées. Par exemple pour définir :

Créer ses propres décorateurs dépasse le cadre de cette introduction, sachez juste les reconnaître et utiliser ceux disponibles dans les bibliothèques que vous utilisez. Si le sujet vous intéresse vous pourrez cependant regarder le lien suivant :

Autres méthodes spéciales

Il existe une foultitude de méthodes spéciales en python permettant de rendre vos objets plus agréable à coder :

On peut citer :

Code final

Notre compteur a bien évolué depuis sa première mouture. Il permet maintenant d'être utilisé de façon bien plus intuitive.

class Compteur:
    @classmethod
    depuis_compteur(cls, compteur):
        return cls(compteur.pas, compteur.valeur)

    def __init__(self, pas=1, valeur=0):
        assert pas != 0

        self._valeur = valeur
        self._pas = pas

    def _get_valeur(self):
        return self._valeur

    valeur = property(_get_valeur)

    def _get_pas(self):
        return self._pas

    def _set_pas(self, pas):
        assert pas != 0
        self._pas = pas
    
    pas = property(_get_pas, _set_pas)


    def incrémente(self):
        self.valeur = self.valeur + self.pas

    def __repr__(self):
        return "Compteur(pas=" + str(self.pas) + ", valeur=" + str(self.valeur) + ")"

    def __str__(self):
        return "Le compteur vaut " + str(self.valeur)

    def __lt__(self, other):
        return self.valeur < other.valeur

    def __gt__(self, other):
        return other.valeur < self.valeur

    def __eq__(self, other):
        return other.valeur == self.valeur