Mis à jour le vendredi 17 novembre 2017
  • 40 heures
  • Difficile

Ce cours est visible gratuitement en ligne.

Ce cours existe en livre papier.

Vous pouvez obtenir un certificat de réussite à l'issue de ce cours.

Vous pouvez être accompagné et mentoré par un professeur particulier par visioconférence sur ce cours.

J'ai tout compris !

Les méthodes spéciales

Connectez-vous ou inscrivez-vous gratuitement pour bénéficier de toutes les fonctionnalités de ce cours !

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 commemon_objet1 + mon_objet2, voiremon_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_attributet 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 fonctionrepr, 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 avecprint. 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 constructeurstr.

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 tapezobjet.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 versself.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 écrivezobjet.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 classeobject. Cela veut dire essentiellement qu'elles reprennent les mêmes méthodes. La classeobjectest 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 classeobject.

La plupart des méthodes spéciales sont déclarées dansobject. Si vous faites par exempleobjet.attribut = valeursans avoir défini de méthode__setattr__dans votre classe, c'est la méthode__setattr__de la classeobjectqui 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 deobject. 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 faireself.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 classeobject, 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 faisantdel objet.attributpar exemple. Elle prend en paramètre, outreself, 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éthodedel self.attribut. Sinon, vous risquez de mettre Python très en colère ! Passez parobject.__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 appelerZDict. 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 typeZDictet qu'on voudra faireobjet[index], à l'intérieur de la classe on feraself._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éincomme une liste ou un dictionnaire, vous devez redéfinir cette méthode__contains__qui prend en paramètre, outreself, l'objet qui nous intéresse. Si l'objet est dans le conteneur, on doit renvoyerTrue; sinonFalse.

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 fonctionlen.

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éthodeformatdans la méthode__str__, sachez simplement que le but est de voir la durée sous la formeMM:SS; pour plus d'informations sur le formatage des chaînes, vous pouvez consulter la documentation de Python.

Créons un premier objetDureeque nous appelonsd1.

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

Si vous essayez de faired1 + 4, par exemple, vous allez obtenir une erreur. Python ne sait pas comment additionner un typeDureeet unint. 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'objetDureequi 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 detype(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 dansself. 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, remplacezd2 = d1 + 54pard2 = 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 écrireobjet1 + objet2ne revient pas au même qu'écrireobjet2 + objet1si 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 classeDuree, si on écritd1 + 4cela fonctionne, alors que4 + d1ne marche pas. En effet, la classintne sait pas quoi faire de votre objetDuree.

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 unr.

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 écrire4 + d1, cela revient au même qued1 + 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 uni.

Exemple de méthode__iadd__pour notre classeDuree:

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 classeintle 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 (TrueouFalse).

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). RenvoieTruesiselfetobjet_a_comparersont égaux,Falsesinon.

!=

def __ne__(self, objet_a_comparer):

Différent de (non equal). RenvoieTruesiselfetobjet_a_comparersont différents,Falsesinon.

>

def __gt__(self, objet_a_comparer):

Teste siselfest strictement supérieur (greater than) àobjet_a_comparer.

>=

def __ge__(self, objet_a_comparer):

Teste siselfest supérieur ou égal (greater or equal) àobjet_a_comparer.

<

def __lt__(self, objet_a_comparer):

Teste siselfest strictement inférieur (lower than) àobjet_a_comparer.

<=

def __le__(self, objet_a_comparer):

Teste siselfest 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 à faireobjet1 < objet2, il essayera l'opération inverse, soitobjet2 >= 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 depickle, 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 modulepickle,__getstate__va être appelée juste avant l'enregistrement.

Si aucune méthode__getstate__n'est définie,pickleenregistre le dictionnaire des attributs de l'objet à enregistrer. Vous vous rappelez ? Il est contenu dansobjet.__dict__.

Sinon,pickleenregistre 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 àpickleet que vous le récupérez ensuite depuis le fichier, vous constatez que l'attributattribut_temporaireest à0, peu importe sa valeur d'origine.

Voyons le code de__getstate__. La méthode ne prend aucun argument (exceptéselfpuisque c'est une méthode d'instance).

Elle enregistre le dictionnaire des attributs dans une variable localedict_attr. Ce dictionnaire a le même contenu queself.__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 modifierattribut_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'attributattribut_temporaire. Étant donné quedict_attretself.__dict__n'ont pas la même référence, l'attribut n'est changé que dansdict_attret le dictionnaire deselfn'est pas modifié.

Enfin, on renvoiedict_attr. Au lieu d'enregistrer dans notre fichierself.__dict__,pickleenregistre 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 manipulezself.__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èreunpickler.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 attributattribut_temporaireavec une valeur quelconque (on ne sait pas laquelle) mais après avoir récupéré l'objet qui est déjà instancié (et avant le retour de la désérialisation !), 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.

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