Apprenez à programmer en Python

Apprenez à programmer en Python

Mis à jour le vendredi 20 juin 2014

Les méthodes spéciales sont des méthodes d'instance que Python reconnaît et sait utiliser, dans certains contextes. Elles peuvent servir à indiquer à Python ce qu'il doit faire quand il se retrouve devant une expression comme mon_objet1 + mon_objet2, voire mon_objet[indice]. Et, encore plus fort, elles contrôlent la façon dont un objet se crée, ainsi que l'accès à ses attributs.

Bref, encore une fonctionnalité puissante et utile du langage, que je vous invite à découvrir. Prenez note du fait que je ne peux pas expliquer dans ce chapitre la totalité des méthodes spéciales. Il y en a qui ne sont pas de notre niveau, il y en a sur lesquelles je passerai plus vite que d'autres. En cas de doute, ou si vous êtes curieux, je vous encourage d'autant plus à aller faire un tour sur le site officiel de Python.

Édition de l'objet et accès aux attributs

Vous avez déjà vu, dès le début de cette troisième partie, un exemple de méthode spéciale. Pour ceux qui ont la mémoire courte, il s'agit de notre constructeur. Une méthode spéciale, en Python, voit son nom entouré de part et d'autre par deux signes « souligné » _. Le nom d'une méthode spéciale prend donc la forme : __methodespeciale__.

Pour commencer, nous allons voir les méthodes qui travaillent directement sur l'objet. Nous verrons ensuite, plus spécifiquement, les méthodes qui permettent d'accéder aux attributs.

Édition de l'objet

Les méthodes que nous allons voir permettent de travailler sur l'objet. Elles interviennent au moment de le créer et au moment de le supprimer. La première, vous devriez la reconnaître : c'est notre constructeur. Elle s'appelle __init__, prend un nombre variable d'arguments et permet de contrôler la création de nos attributs.

class Exemple:
    """Un petit exemple de classe"""
    def __init__(self, nom):
        """Exemple de constructeur"""
        self.nom = nom
        self.autre_attribut = "une valeur"

Pour créer notre objet, nous utilisons le nom de la classe et nous passons, entre parenthèses, les informations qu'attend notre constructeur :

mon_objet = Exemple("un premier exemple")

J'ai un peu simplifié ce qui se passe mais, pour l'instant, c'est tout ce qu'il vous faut retenir. Comme vous pouvez le voir, à partir du moment où l'objet est créé, on peut accéder à ses attributs grâce à mon_objet.nom_attribut et exécuter ses méthodes grâce à mon_objet.nom_methode(…).

Il existe également une autre méthode, __del__, qui va être appelée au moment de la destruction de l'objet.

La destruction ? Quand un objet se détruit-il ?

Bonne question. Il y a plusieurs cas : d'abord, quand vous voulez le supprimer explicitement, grâce au mot-clé del (del mon_objet). Ensuite, si l'espace de noms contenant l'objet est détruit, l'objet l'est également. Par exemple, si vous instanciez l'objet dans le corps d'une fonction : à la fin de l'appel à la fonction, la méthode __del__ de l'objet sera appelée. Enfin, si votre objet résiste envers et contre tout pendant l'exécution du programme, il sera supprimé à la fin de l'exécution.

def __del__(self):
        """Méthode appelée quand l'objet est supprimé"""
        print("C'est la fin ! On me supprime !")

À quoi cela peut-il bien servir, de contrôler la destruction d'un objet ?

Souvent, à rien. Python s'en sort comme un grand garçon, il n'a pas besoin d'aide. Parfois, on peut vouloir récupérer des informations d'état sur l'objet au moment de sa suppression. Mais ce n'est qu'un exemple : les méthodes spéciales sont un moyen d'exécuter des actions personnalisées sur certains objets, dans un cas précis. Si l'utilité ne saute pas aux yeux, vous pourrez en trouver une un beau jour, en codant votre projet.

Souvenez-vous que si vous ne définissez pas de méthode spéciale pour telle ou telle action, Python aura un comportement par défaut dans le contexte où cette méthode est appelée. Écrire une méthode spéciale permet de modifier ce comportement par défaut. Dans l'absolu, vous n'êtes même pas obligés d'écrire un constructeur.

Représentation de l'objet

Nous allons voir deux méthodes spéciales qui permettent de contrôler comment l'objet est représenté et affiché. Vous avez sûrement déjà pu constater que, quand on instancie des objets issus de nos propres classes, si on essaye de les afficher directement dans l'interpréteur ou grâce à print, on obtient quelque chose d'assez laid :

<__main__.XXX object at 0x00B46A70>

On a certes les informations utiles, mais pas forcément celles qu'on veut, et l'ensemble n'est pas magnifique, il faut bien le reconnaître.

La première méthode permettant de remédier à cet état de fait est __repr__. Elle affecte la façon dont est affiché l'objet quand on tape directement son nom. On la redéfinit quand on souhaite faciliter le debug sur certains objets :

class Personne:
    """Classe représentant une personne"""
    def __init__(self, nom, prenom):
        """Constructeur de notre classe"""
        self.nom = nom
        self.prenom = prenom
        self.age = 33
    def __repr__(self):
        """Quand on entre notre objet dans l'interpréteur"""
        return "Personne: nom({}), prénom({}), âge({})".format(
                self.nom, self.prenom, self.age)

Et le résultat en images :

>>> p1 = Personne("Micado", "Jean")
>>> p1
Personne: nom(Micado), prénom(Jean), âge(33)
>>>

Comme vous le voyez, la méthode __repr__ ne prend aucun paramètre (sauf, bien entendu, self) et renvoie une chaîne de caractères : la chaîne à afficher quand on entre l'objet directement dans l'interpréteur.

On peut également obtenir cette chaîne grâce à la fonction repr, qui se contente d'appeler la méthode spéciale __repr__ de l'objet passé en paramètre :

>>> p1 = Personne("Micado", "Jean")
>>> repr(p1)
'Personne: nom(Micado), prénom(Jean), âge(33)'
>>>

Il existe une seconde méthode spéciale, __str__, spécialement utilisée pour afficher l'objet avec print. Par défaut, si aucune méthode __str__ n'est définie, Python appelle la méthode __repr__ de l'objet. La méthode __str__ est également appelée si vous désirez convertir votre objet en chaîne avec le constructeur str.

class Personne:
    """Classe représentant une personne"""
    def __init__(self, nom, prenom):
        """Constructeur de notre classe"""
        self.nom = nom
        self.prenom = prenom
        self.age = 33
    def __str__(self):
        """Méthode permettant d'afficher plus joliment notre objet"""
        return "{} {}, âgé de {} ans".format(
                self.prenom, self.nom, self.age)

Et en pratique :

>>> p1 = Personne("Micado", "Jean")
>>> print(p1)
Jean Micado, âgé de 33 ans
>>> chaine = str(p1)
>>> chaine
'Jean Micado, âgé de 33 ans'
>>>

Accès aux attributs de notre objet

Nous allons découvrir trois méthodes permettant de définir comment accéder à nos attributs et les modifier.

La méthode __getattr__

La méthode spéciale __getattr__ permet de définir une méthode d'accès à nos attributs plus large que celle que Python propose par défaut. En fait, cette méthode est appelée quand vous tapez objet.attribut (non pas pour modifier l'attribut mais simplement pour y accéder). Python recherche l'attribut et, s'il ne le trouve pas dans l'objet et si une méthode __getattr__ existe, il va l'appeler en lui passant en paramètre le nom de l'attribut recherché, sous la forme d'une chaîne de caractères.

Un petit exemple ?

>>> class Protege:
...     """Classe possédant une méthode particulière d'accès à ses attributs :
...     Si l'attribut n'est pas trouvé, on affiche une alerte et renvoie None"""
...
...     
...     def __init__(self):
...         """On crée quelques attributs par défaut"""
...         self.a = 1
...         self.b = 2
...         self.c = 3
...     def __getattr__(self, nom):
...         """Si Python ne trouve pas l'attribut nommé nom, il appelle
...         cette méthode. On affiche une alerte"""
...
...         
...         print("Alerte ! Il n'y a pas d'attribut {} ici !".format(nom))
...
>>> pro = Protege()
>>> pro.a
1
>>> pro.c
3
>>> pro.e
Alerte ! Il n'y a pas d'attribut e ici !
>>>

Vous comprenez le principe ? Si l'attribut auquel on souhaite accéder existe, notre méthode n'est pas appelée. En revanche, si l'attribut n'existe pas, notre méthode __getattr__ est appelée. On lui passe en paramètre le nom de l'attribut auquel Python essaye d'accéder. Ici, on se contente d'afficher une alerte. Mais on pourrait tout aussi bien rediriger vers un autre attribut. Par exemple, si on essaye d'accéder à un attribut qui n'existe pas, on redirige vers self.c. Je vous laisse faire l'essai, cela n'a rien de difficile.

La méthode __setattr__

Cette méthode définit l'accès à un attribut destiné à être modifié. Si vous écrivez objet.nom_attribut = nouvelle_valeur, la méthode spéciale __setattr__ sera appelée ainsi : objet.__setattr__("nom_attribut", nouvelle_valeur). Là encore, le nom de l'attribut recherché est passé sous la forme d'une chaîne de caractères. Cette méthode permet de déclencher une action dès qu'un attribut est modifié, par exemple enregistrer l'objet :

def __setattr__(self, nom_attr, val_attr):
        """Méthode appelée quand on fait objet.nom_attr = val_attr.
        On se charge d'enregistrer l'objet"""
        
        
        object.__setattr__(self, nom_attr, val_attr)
        self.enregistrer()

Une explication s'impose concernant la ligne 6, je pense. Je vais faire de mon mieux, sachant que j'expliquerai bien plus en détail, au prochain chapitre, le concept d'héritage. Pour l'instant, il vous suffit de savoir que toutes les classes que nous créons sont héritées de la classe object. Cela veut dire essentiellement qu'elles reprennent les mêmes méthodes. La classe object est définie par Python. Je disais plus haut que, si vous ne définissiez pas une certaine méthode spéciale, Python avait un comportement par défaut : ce comportement est défini par la classe object.

La plupart des méthodes spéciales sont déclarées dans object. Si vous faites par exemple objet.attribut = valeur sans avoir défini de méthode __setattr__ dans votre classe, c'est la méthode __setattr__ de la classe object qui sera appelée.

Mais si vous redéfinissez la méthode __setattr__ dans votre classe, la méthode appelée sera alors celle que vous définissez, et non celle de object. Oui mais… vous ne savez pas comment Python fait, réellement, pour modifier la valeur d'un attribut. Le mécanisme derrière la méthode vous est inconnu.

Si vous essayez, dans la méthode __setattr__, de faire self.attribut = valeur, vous allez créer une jolie erreur : Python va vouloir modifier un attribut, il appelle la méthode __setattr__ de la classe que vous avez définie, il tombe dans cette méthode sur une nouvelle affectation d'attribut, il appelle donc de nouveau __setattr__… et tout cela, jusqu'à l'infini ou presque. Python met en place une protection pour éviter qu'une méthode ne s'appelle elle-même à l'infini, mais cela ne règle pas le problème.

Tout cela pour dire que, dans votre méthode __setattr__, vous ne pouvez pas modifier d'attribut de la façon que vous connaissez. Si vous le faites, __setattr__ appellera __setattr__ qui appellera __setattr__… à l'infini. Donc si on souhaite modifier un attribut, on va se référer à la méthode __setattr__ définie dans la classe object, la classe mère dont toutes nos classes héritent.

Si toutes ces explications vous ont paru plutôt dures, ne vous en faites pas trop : je détaillerai au prochain chapitre ce qu'est l'héritage, vous comprendrez sûrement mieux à ce moment.

La méthode __delattr__

Cette méthode spéciale est appelée quand on souhaite supprimer un attribut de l'objet, en faisant del objet.attribut par exemple. Elle prend en paramètre, outre self, le nom de l'attribut que l'on souhaite supprimer. Voici un exemple d'une classe dont on ne peut supprimer aucun attribut :

def __delattr__(self, nom_attr):
        """On ne peut supprimer d'attribut, on lève l'exception
        AttributeError"""
        
        raise AttributeError("Vous ne pouvez supprimer aucun attribut de cette classe")

Là encore, si vous voulez supprimer un attribut, n'utilisez pas dans votre méthode del self.attribut. Sinon, vous risquez de mettre Python très en colère ! Passez par object.__delattr__ qui sait mieux que nous comment tout cela fonctionne.

Un petit bonus

Voici quelques fonctions qui font à peu près ce que nous avons fait mais en utilisant des chaînes de caractères pour les noms d'attributs. Vous pourrez en avoir l'usage :

objet = MaClasse() # On crée une instance de notre classe
getattr(objet, "nom") # Semblable à objet.nom
setattr(objet, "nom", val) # = objet.nom = val ou objet.__setattr__("nom", val)
delattr(objet, "nom") # = del objet.nom ou objet.__delattr__("nom")
hasattr(objet, "nom") # Renvoie True si l'attribut "nom" existe, False sinon

Peut-être ne voyez-vous pas trop l'intérêt de ces fonctions qui prennent toutes, en premier paramètre, l'objet sur lequel travailler et en second le nom de l'attribut (sous la forme d'une chaîne). Toutefois, cela peut être très pratique parfois de travailler avec des chaînes de caractères plutôt qu'avec des noms d'attributs. D'ailleurs, c'est un peu ce que nous venons de faire, dans nos redéfinitions de méthodes accédant aux attributs.

Là encore, si l'intérêt ne saute pas aux yeux, laissez ces fonctions de côté. Vous pourrez les retrouver par la suite.

Les méthodes de conteneur

Nous allons commencer à travailler sur ce que l'on appelle la surcharge d'opérateurs. Il s'agit assez simplement d'expliquer à Python quoi faire quand on utilise tel ou tel opérateur. Nous allons ici voir quatre méthodes spéciales qui interviennent quand on travaille sur des objets conteneurs.

Accès aux éléments d'un conteneur

Les objets conteneurs, j'espère que vous vous en souvenez, ce sont les chaînes de caractères, les listes et les dictionnaires, entre autres. Tous ont un point commun : ils contiennent d'autres objets, auxquels on peut accéder grâce à l'opérateur [].

Les trois premières méthodes que nous allons voir sont __getitem__, __setitem__ et __delitem__. Elles servent respectivement à définir quoi faire quand on écrit :

  • objet[index] ;

  • objet[index] = valeur ;

  • del objet[index];

Pour cet exemple, nous allons voir une classe enveloppe de dictionnaire. Les classes enveloppes sont des classes qui ressemblent à d'autres classes mais n'en sont pas réellement. Cela vous avance ?

Nous allons créer une classe que nous allons appeler ZDict. Elle va posséder un attribut auquel on ne devra pas accéder de l'extérieur de la classe, un dictionnaire que nous appellerons _dictionnaire. Quand on créera un objet de type ZDict et qu'on voudra faire objet[index], à l'intérieur de la classe on fera self._dictionnaire[index]. En réalité, notre classe fera semblant d'être un dictionnaire, elle réagira de la même manière, mais elle n'en sera pas réellement un.

class ZDict:
    """Classe enveloppe d'un dictionnaire"""
    def __init__(self):
        """Notre classe n'accepte aucun paramètre"""
        self._dictionnaire = {}
    def __getitem__(self, index):
        """Cette méthode spéciale est appelée quand on fait objet[index]
        Elle redirige vers self._dictionnaire[index]"""
        
        return self._dictionnaire[index]
    def __setitem__(self, index, valeur):
        """Cette méthode est appelée quand on écrit objet[index] = valeur
        On redirige vers self._dictionnaire[index] = valeur"""
        
        self._dictionnaire[index] = valeur

Vous avez un exemple d'utilisation des deux méthodes __getitem__ et __setitem__ qui, je pense, est assez clair. Pour __delitem__, je crois que c'est assez évident, elle ne prend qu'un seul paramètre qui est l'index que l'on souhaite supprimer. Vous pouvez étendre cet exemple avec d'autres méthodes que nous avons vues plus haut, notamment __repr__ et __str__. N'hésitez pas, entraînez-vous, tout cela peut vous servir.

La méthode spéciale derrière le mot-clé in

Il existe une quatrième méthode, appelée __contains__, qui est utilisée quand on souhaite savoir si un objet se trouve dans un conteneur.

Exemple classique :

ma_liste = [1, 2, 3, 4, 5]
8 in ma_liste # Revient au même que ...
ma_liste.__contains__(8)

Ainsi, si vous voulez que votre classe enveloppe puisse utiliser le mot-clé in comme une liste ou un dictionnaire, vous devez redéfinir cette méthode __contains__ qui prend en paramètre, outre self, l'objet qui nous intéresse. Si l'objet est dans le conteneur, on doit renvoyer True ; sinon False.

Je vous laisse redéfinir cette méthode, vous avez toutes les indications nécessaires.

Connaître la taille d'un conteneur

Il existe enfin une méthode spéciale __len__, appelée quand on souhaite connaître la taille d'un objet conteneur, grâce à la fonction len.

len(objet) équivaut à objet.__len__(). Cette méthode spéciale ne prend aucun paramètre et renvoie une taille sous la forme d'un entier. Là encore, je vous laisse faire l'essai.

Les méthodes mathématiques

Pour cette section, nous allons continuer à voir les méthodes spéciales permettant la surcharge d'opérateurs mathématiques, comme +, -, * et j'en passe.

Ce qu'il faut savoir

Pour cette section, nous allons utiliser un nouvel exemple, une classe capable de contenir des durées. Ces durées seront contenues sous la forme d'un nombre de minutes et un nombre de secondes.

Voici le corps de la classe, gardez-le sous la main :

class Duree:
    """Classe contenant des durées sous la forme d'un nombre de minutes
    et de secondes"""
    
    def __init__(self, min=0, sec=0):
        """Constructeur de la classe"""
        self.min = min # Nombre de minutes
        self.sec = sec # Nombre de secondes
    def __str__(self):
        """Affichage un peu plus joli de nos objets"""
        return "{0:02}:{1:02}".format(self.min, self.sec)

On définit simplement deux attributs contenant notre nombre de minutes et notre nombre de secondes, ainsi qu'une méthode pour afficher tout cela un peu mieux. Si vous vous interrogez sur l'utilisation de la méthode format dans la méthode __str__, sachez simplement que le but est de voir la durée sous la forme MM:SS ; pour plus d'informations sur le formatage des chaînes, vous pouvez consulter la documentation de Python.

Créons un premier objet Duree que nous appelons d1.

>>> d1 = Duree(3, 5)
>>> print(d1)
03:05
>>>

Si vous essayez de faire d1 + 4, par exemple, vous allez obtenir une erreur. Python ne sait pas comment additionner un type Duree et un int. Il ne sait même pas comment ajouter deux durées ! Nous allons donc lui expliquer.

La méthode spéciale à redéfinir est __add__. Elle prend en paramètre l'objet que l'on souhaite ajouter. Voici deux lignes de code qui reviennent au même :

d1 + 4
d1.__add__(4)

Comme vous le voyez, quand vous utilisez le symbole + ainsi, c'est en fait la méthode __add__ de l'objet Duree qui est appelée. Elle prend en paramètre l'objet que l'on souhaite ajouter, peu importe le type de l'objet en question. Et elle doit renvoyer un objet exploitable, ici il serait plus logique que ce soit une nouvelle durée.

Si vous devez faire différentes actions en fonction du type de l'objet à ajouter, testez le résultat de type(objet_a_ajouter).

def __add__(self, objet_a_ajouter):
        """L'objet à ajouter est un entier, le nombre de secondes"""
        nouvelle_duree = Duree()
        # On va copier self dans l'objet créé pour avoir la même durée
        nouvelle_duree.min = self.min
        nouvelle_duree.sec = self.sec
        # On ajoute la durée
        nouvelle_duree.sec += objet_a_ajouter
        # Si le nombre de secondes >= 60
        if nouvelle_duree.sec >= 60:
            nouvelle_duree.min += nouvelle_duree.sec // 60
            nouvelle_duree.sec = nouvelle_duree.sec % 60
        # On renvoie la nouvelle durée
        return nouvelle_duree

Prenez le temps de comprendre le mécanisme et le petit calcul pour vous assurer d'avoir une durée cohérente. D'abord, on crée une nouvelle durée qui est l'équivalent de la durée contenue dans self. On l'augmente du nombre de secondes à ajouter et on s'assure que le temps est cohérent (le nombre de secondes n'atteint pas 60). Si le temps n'est pas cohérent, on le corrige. On renvoie enfin notre nouvel objet modifié. Voici un petit code qui montre comment utiliser notre méthode :

>>> d1 = Duree(12, 8)
>>> print(d1)
12:08
>>> d2 = d1 + 54 # d1 + 54 secondes
>>> print(d2)
13:02
>>>

Pour mieux comprendre, remplacez d2 = d1 + 54 par d2 = d1.__add__(54) : cela revient au même. Ce remplacement ne sert qu'à bien comprendre le mécanisme. Il va de soi que ces méthodes spéciales ne sont pas à appeler directement depuis l'extérieur de la classe, les opérateurs n'ont pas été inventés pour rien.

Sachez que sur le même modèle, il existe les méthodes :

  • __sub__ : surcharge de l'opérateur - ;

  • __mul__ : surcharge de l'opérateur * ;

  • __truediv__ : surcharge de l'opérateur / ;

  • __floordiv__ : surcharge de l'opérateur // (division entière) ;

  • __mod__ : surcharge de l'opérateur % (modulo) ;

  • __pow__ : surcharge de l'opérateur ** (puissance) ;

Il y en a d'autres que vous pouvez consulter sur le site web de Python.

Tout dépend du sens

Vous l'avez peut-être remarqué, et c'est assez logique si vous avez suivi mes explications, mais écrire objet1 + objet2 ne revient pas au même qu'écrire objet2 + objet1 si les deux objets ont des types différents.

En effet, suivant le cas, c'est la méthode __add__ de l'un ou l'autre des objets qui est appelée.

Cela signifie que, lorsqu'on utilise la classe Duree, si on écrit d1 + 4 cela fonctionne, alors que 4 + d1 ne marche pas. En effet, la class int ne sait pas quoi faire de votre objet Duree.

Il existe cependant une panoplie de méthodes spéciales pour faire le travail de __add__ si vous écrivez l'opération dans l'autre sens. Il suffit de préfixer le nom des méthodes spéciales par un r.

def __radd__(self, objet_a_ajouter):
        """Cette méthode est appelée si on écrit 4 + objet et que
        le premier objet (4 dans cet exemple) ne sait pas comment ajouter
        le second. On se contente de rediriger sur __add__ puisque,
        ici, cela revient au même : l'opération doit avoir le même résultat,
        posée dans un sens ou dans l'autre"""
        
        return self + objet_a_ajouter

À présent, on peut écrire 4 + d1, cela revient au même que d1 + 4.

N'hésitez pas à relire ces exemples s'ils vous paraissent peu clairs.

D'autres opérateurs

Il est également possible de surcharger les opérateurs +=, -=, etc. On préfixe cette fois-ci les noms de méthode que nous avons vus par un i.

Exemple de méthode __iadd__ pour notre classe Duree :

def __iadd__(self, objet_a_ajouter):
        """L'objet à ajouter est un entier, le nombre de secondes"""
        # On travaille directement sur self cette fois
        # On ajoute la durée
        self.sec += objet_a_ajouter
        # Si le nombre de secondes >= 60
        if self.sec >= 60:
            self.min += self.sec // 60
            self.sec = self.sec % 60
        # On renvoie self
        return self

Et en images :

>>> d1 = Duree(8, 5)
>>> d1 += 128
>>> print(d1)
10:13
>>>

Je ne peux que vous encourager à faire des tests, pour être bien sûrs de comprendre le mécanisme. Je vous ai donné ici une façon de faire en la commentant mais, si vous ne pratiquez pas ou n'essayez pas par vous-mêmes, vous n'allez pas la retenir et vous n'allez pas forcément comprendre la logique.

Les méthodes de comparaison

Pour finir, nous allons voir la surcharge des opérateurs de comparaison que vous connaissez depuis quelque temps maintenant : ==, !=, <, >, <=, >=.

Ces méthodes sont donc appelées si vous tentez de comparer deux objets entre eux. Comment Python sait-il que 3 est inférieur à 18 ? Une méthode spéciale de la classe int le permet, en simplifiant. Donc si vous voulez comparer des durées, par exemple, vous allez devoir redéfinir certaines méthodes que je vais présenter plus bas. Elles devront prendre en paramètre l'objet à comparer à self, et doivent renvoyer un booléen (True ou False).

Je vais me contenter de vous faire un petit tableau récapitulatif des méthodes à redéfinir pour comparer deux objets entre eux :

Opérateur

Méthode spéciale

Résumé

==

def __eq__(self, objet_a_comparer):

Opérateur d'égalité (equal). Renvoie True si self et objet_a_comparer sont égaux, False sinon.

!=

def __ne__(self, objet_a_comparer):

Différent de (non equal). Renvoie True si self et objet_a_comparer sont différents, False sinon.

>

def __gt__(self, objet_a_comparer):

Teste si self est strictement supérieur (greather than) à objet_a_comparer.

>=

def __ge__(self, objet_a_comparer):

Teste si self est supérieur ou égal (greater or equal) à objet_a_comparer.

<

def __lt__(self, objet_a_comparer):

Teste si self est strictement inférieur (lower than) à objet_a_comparer.

<=

def __le__(self, objet_a_comparer):

Teste si self est inférieur ou égal (lower or equal) à objet_a_comparer.

Sachez que ce sont ces méthodes spéciales qui sont appelées si, par exemple, vous voulez trier une liste contenant vos objets.

Sachez également que, si Python n'arrive pas à faire objet1 < objet2, il essayera l'opération inverse, soit objet2 >= objet1. Cela vaut aussi pour les autres opérateurs de comparaison que nous venons de voir.

Allez, je vais vous mettre deux exemples malgré tout, il ne tient qu'à vous de redéfinir les autres méthodes présentées plus haut :

def __eq__(self, autre_duree):
        """Test si self et autre_duree sont égales"""
        return self.sec == autre_duree.sec and self.min == autre_duree.min
    def __gt__(self, autre_duree):
        """Test si self > autre_duree"""
        # On calcule le nombre de secondes de self et autre_duree
        nb_sec1 = self.sec + self.min * 60
        nb_sec2 = autre_duree.sec + autre_duree.min * 60
        return nb_sec1 > nb_sec2

Ces exemples devraient vous suffire, je pense.

Des méthodes spéciales utiles à pickle

Vous vous souvenez de pickle, j'espère. Pour conclure ce chapitre sur les méthodes spéciales, nous allons en voir deux qui sont utilisées par ce module pour influencer la façon dont nos objets sont enregistrés dans des fichiers.

Prenons un cas concret, d'une utilité pratique discutable.

On crée une classe qui va contenir plusieurs attributs. Un de ces attributs possède une valeur temporaire, qui n'est utile que pendant l'exécution du programme. Si on arrête ce programme et qu'on le relance, on doit récupérer le même objet mais la valeur temporaire doit être remise à 0, par exemple.

Il y a d'autres moyens d'y parvenir, je le reconnais. Mais les autres applications que j'ai en tête sont plus dures à développer et à expliquer rapidement, donc gardons cet exemple.

La méthode spéciale __getstate__

La méthode __getstate__ est appelée au moment de sérialiser l'objet. Quand vous voulez enregistrer l'objet à l'aide du module pickle, __getstate__ va être appelée juste avant l'enregistrement.

Si aucune méthode __getstate__ n'est définie, pickle enregistre le dictionnaire des attributs de l'objet à enregistrer. Vous vous rappelez ? Il est contenu dans objet.__dict__.

Sinon, pickle enregistre dans le fichier la valeur renvoyée par __getstate__ (généralement, un dictionnaire d'attributs modifié).

Voyons un peu comment coder notre exemple grâce à __getstate__ :

class Temp:
    """Classe contenant plusieurs attributs, dont un temporaire"""
    
    def __init__(self):
        """Constructeur de notre objet"""
        self.attribut_1 = "une valeur"
        self.attribut_2 = "une autre valeur"
        self.attribut_temporaire = 5
   
    def __getstate__(self):
        """Renvoie le dictionnaire d'attributs à sérialiser"""
        dict_attr = dict(self.__dict__)
        dict_attr["attribut_temporaire"] = 0
        return dict_attr

Avant de revenir sur le code, vous pouvez en voir les effets. Si vous tentez d'enregistrer cet objet grâce à pickle et que vous le récupérez ensuite depuis le fichier, vous constatez que l'attribut attribut_temporaire est à 0, peu importe sa valeur d'origine.

Voyons le code de __getstate__. La méthode ne prend aucun argument (excepté self puisque c'est une méthode d'instance).

Elle enregistre le dictionnaire des attributs dans une variable locale dict_attr. Ce dictionnaire a le même contenu que self.__dict__ (le dictionnaire des attributs de l'objet). En revanche, il a une référence différente. Sans cela, à la ligne suivante, au moment de modifier attribut_temporaire, le changement aurait été également appliqué à l'objet, ce que l'on veut éviter.

À la ligne suivante, donc, on change la valeur de l'attribut attribut_temporaire. Étant donné que dict_attr et self.__dict__ n'ont pas la même référence, l'attribut n'est changé que dans dict_attr et le dictionnaire de self n'est pas modifié.

Enfin, on renvoie dict_attr. Au lieu d'enregistrer dans notre fichier self.__dict__, pickle enregistre notre dictionnaire modifié, dict_attr.

Si ce n'est pas assez clair, je vous encourage à tester par vous-mêmes, essayez de modifier la méthode __getstate__ et manipulez self.__dict__ pour bien comprendre le code.

La méthode __setstate__

À la différence de __getstate__, la méthode __setstate__ est appelée au moment de désérialiser l'objet. Concrètement, si vous récupérez un objet à partir d'un fichier sérialisé, __setstate__ sera appelée après la récupération du dictionnaire des attributs.

Pour schématiser, voici l'exécution que l'on va observer derrière unpickler.load() :

  1. L'objet Unpickler lit le fichier.

  2. Il récupère le dictionnaire des attributs. Je vous rappelle que si aucune méthode __getstate__ n'est définie dans notre classe, ce dictionnaire est celui contenu dans l'attribut spécial __dict__ de l'objet au moment de sa sérialisation.

  3. Ce dictionnaire récupéré est envoyé à la méthode __setstate__ si elle existe. Si elle n'existe pas, Python considère que c'est le dictionnaire des attributs de l'objet à récupérer et écrit donc l'attribut __dict__ de l'objet en y plaçant ce dictionnaire récupéré.

Le même exemple mais, cette fois, par la méthode __setstate__ :

...
    def __setstate__(self, dict_attr):
        """Méthode appelée lors de la désérialisation de l'objet"""
        dict_attr["attribut_temporaire"] = 0
        self.__dict__ = dict_attr

Quelle est la différence entre les deux méthodes que nous avons vues ?

L'objectif que nous nous étions fixé peut être atteint par ces deux méthodes. Soit notre classe met en œuvre une méthode __getstate__, soit elle met en œuvre une méthode __setstate__.

Dans le premier cas, on modifie le dictionnaire des attributs avant la sérialisation. Le dictionnaire des attributs enregistré est celui que nous avons modifié avec la valeur de notre attribut temporaire à 0.

Dans le second cas, on modifie le dictionnaire d'attributs après la désérialisation. Le dictionnaire que l'on récupère contient un attribut attribut_temporaire avec une valeur quelconque (on ne sait pas laquelle) mais avant de récupérer l'objet, on met cette valeur à 0.

Ce sont deux moyens différents, qui ici reviennent au même. À vous de choisir la meilleure méthode en fonction de vos besoins (les deux peuvent être présentes dans la même classe si nécessaire).

Là encore, je vous encourage à faire des essais si ce n'est pas très clair.

On peut enregistrer dans un fichier autre chose que des dictionnaires

Votre méthode __getstate__ n'est pas obligée de renvoyer un dictionnaire d'attributs. Elle peut renvoyer un autre objet, un entier, un flottant, mais dans ce cas une méthode __setstate__ devra exister pour savoir « quoi faire » avec l'objet enregistré. Si ce n'est pas un dictionnaire d'attributs, Python ne peut pas le deviner !

Là encore, je vous laisse tester si cela vous intéresse.

Je veux encore plus puissant !

__getstate__ et __setstate__ sont les deux méthodes les plus connues pour agir sur la sérialisation d'objets. Mais il en existe d'autres, plus complexes.

Si vous êtes intéressés, jetez un œil du côté de la PEP 307.

En résumé

  • Les méthodes spéciales permettent d'influencer la manière dont Python accède aux attributs d'une instance et réagit à certains opérateurs ou conversions.

  • Les méthodes spéciales sont toutes entourées de deux signes « souligné » (_).

  • Les méthodes __getattr__, __setattr__ et __delattr__ contrôlent l'accès aux attributs de l'instance.

  • Les méthodes __getitem__, __setitem__ et __delitem__ surchargent l'indexation ([]).

  • Les méthodes __add__, __sub__, __mul__… surchargent les opérateurs mathématiques.

  • Les méthodes __eq__, __ne__, __gt__… surchargent les opérateurs de comparaison.

L'auteur

Découvrez aussi ce cours en...

Exemple de certificat de réussite
Exemple de certificat de réussite