Définition
Une classe que vous écrivez démarre plus pauvre que les types de Python. Len, ==, une boucle for fonctionnent sur un dictionnaire ou une liste, jamais sur un objet fait main tant que vous n'avez rien ajouté. Les méthodes magiques comblent cet écart en branchant votre classe sur cette même syntaxe.
Une méthode magique est une méthode dont le nom commence et se termine par deux tirets bas, comme __len__ ou __eq__. Vous ne l'appelez jamais vous-même : c'est Python qui la déclenche à votre place, dès qu'une syntaxe du langage rencontre votre objet. Écrire len(panier) revient à demander panier.__len__(), et la classe qui la définit gagne un comportement que les types natifs possèdent depuis toujours, comme l'illustre ce panier qui répond à len parce qu'il définit __len__, rien de plus.
class Panier:
def __init__(self, articles):
self.articles = articles
def __len__(self):
# Cette méthode répond à ce que demande len(panier)
return len(self.articles)
panier = Panier(["stylo", "cahier"])
print(len(panier))
# 2On les appelle aussi méthodes spéciales, ou dunder methods en anglais, pour double underscore. Le mot magique décrit l'effet ressenti, jamais le mécanisme : c'est une convention de nommage que l'interpréteur connaît par cœur et consulte au bon moment.
Les familles à connaître
Une centaine de méthodes magiques existent dans Python, un chiffre qui peut donner le vertige : faut-il toutes les connaître avant d'écrire une classe utile ? Non, une dizaine couvre l'essentiel, réunies dans le tableau qui suit.
| Méthode | Déclenchée par |
|---|---|
__init__ | La création d'un objet |
__repr__ et __str__ | repr et l'affichage |
__eq__ | L'opérateur d'égalité |
__lt__ | La comparaison stricte, et donc le tri |
__getitem__ | L'accès entre crochets |
__contains__ | Le test d'appartenance |
__iter__ | La boucle for et tout ce qui parcourt |
__enter__ et __exit__ | Le bloc with |
__call__ | Les parenthèses posées après l'objet |
Trier une liste d'objets demande en théorie plusieurs de ces méthodes de comparaison. Le décorateur total_ordering du module functools déduit celles qui manquent à partir de __eq__ et d'une seule autre, par exemple __lt__ : deux méthodes suffisent alors là où six seraient nécessaires.
Un objet devient parcourable dès qu'il expose __iter__, qu'il hérite ou non d'une liste. Un gestionnaire de contexte n'est rien de plus qu'un objet qui expose __enter__ et __exit__, exactement ce que décrit le duck typing : ce qui compte n'est pas la classe dont vous héritez, mais ce que votre objet sait faire quand on le sollicite.
Le piège de l'égalité sans le hachage
Ajouter une méthode __eq__ semble n'engager à rien : vous décrivez comment deux objets se ressemblent. Pourtant cette ligne a un effet de bord que presque personne n'anticipe : Python retire silencieusement le hachage par défaut de la classe, et vos objets ne peuvent plus entrer dans un ensemble ni servir de clé de dictionnaire.
class Client:
def __init__(self, email):
self.email = email
def __eq__(self, autre):
# Deux clients sont égaux si leur email l'est, rien d'autre
return self.email == autre.email
{Client("marie@exemple.fr")}
# TypeError: unhashable type: 'Client'L'erreur n'apparaît jamais en écrivant __eq__, elle attend le jour où quelqu'un range vos objets dans un set ou un dict. Le message TypeError: unhashable type pointe alors vers une ligne sans rapport avec la cause réelle.
La correction tient en une méthode de plus, __hash__, calculée sur les mêmes attributs que l'égalité : deux objets égaux doivent produire le même hachage, sans quoi un dictionnaire pourrait ranger deux fois la même clé sans s'en apercevoir. Une dataclass déclarée avec frozen=True génère les deux méthodes pour vous, et c'est souvent la meilleure réponse.
Deux représentations pour deux lecteurs
Sans rien écrire, afficher un de vos objets dans la console renvoie une ligne du genre <Client object at 0x7f...>, qui ne dit rien du contenu de l'objet. Deux méthodes magiques permettent de reprendre la main sur ce texte, et elles ne s'adressent pas au même lecteur.
__repr__ parle au développeur : elle apparaît dans la console, dans les messages d'erreur et dans l'affichage d'une liste d'objets, et devrait montrer de quoi l'objet est fait. __str__ parle à l'utilisateur final, et c'est elle qu'un affichage ordinaire produit.
class Duree:
def __init__(self, minutes):
self.minutes = minutes
def __repr__(self):
# Pour déboguer : on veut voir la valeur brute
return f"Duree(minutes={self.minutes})"
def __str__(self):
# Pour afficher : on veut un format lisible
return f"{self.minutes // 60} h {self.minutes % 60}"Si vous n'en écrivez qu'une seule, écrivez __repr__ : Python s'en sert comme repli quand __str__ manque, alors que l'inverse n'est jamais vrai. Une classe sans représentation affiche une adresse mémoire, qui ne dit rien pendant un débogage et transforme une liste de résultats en devinette.
Questions fréquentes
Peut-on appeler une méthode magique directement ?
Techniquement oui, panier.__len__() fonctionne. En pratique, mieux vaut ne jamais le faire : la syntaxe du langage lève une TypeError explicite quand la méthode manque, là où l'appel direct produit une erreur d'attribut bien plus difficile à interpréter.
Combien faut-il en définir sur une classe ?
Le moins possible, et __repr__ presque toujours puisqu'il aide au débogage sans coût. Une méthode magique n'a de sens que si la syntaxe qu'elle capte dit vraiment quelque chose de l'objet : donner une addition à ce qui ne s'additionne pas dans la vraie vie rend le code illisible.
Pourquoi mon objet refuse-t-il de se laisser parcourir par une boucle ?
Parce qu'il ne définit ni __iter__ ni __getitem__, les deux seules portes que Python essaie avant d'abandonner et de lever une erreur.