Les méthodes magiques (Dunder) en Python
Apprenez les méthodes magiques Python (__init__, __str__, __repr__), la surcharge d'opérateurs, les protocoles de conteneur et de gestionnaire de contexte.
Les méthodes magiques — également appelées méthodes dunder (abréviation de double underscore) — sont des méthodes spéciales dont les noms commencent et se terminent par deux tirets bas, comme __init__ ou __len__. Elles constituent le système de hooks de Python : en les définissant dans vos propres classes, vous indiquez à Python comment un objet doit se comporter avec les opérateurs et fonctions intégrés tels que +, len(), print(), in et with.
Vous n'appelez jamais les méthodes dunder directement. Au contraire, Python les appelle automatiquement en arrière-plan :
| Expression Python | Dunder appelé |
|---|---|
str(obj) | obj.__str__() |
len(obj) | obj.__len__() |
a + b | a.__add__(b) |
a == b | a.__eq__(b) |
item in obj | obj.__contains__(item) |
with obj as x: | obj.__enter__() / obj.__exit__(...) |
Ce chapitre couvre :
- La représentation sous forme de chaîne —
__repr__et__str__ - Les opérateurs de comparaison —
__eq__,__lt__et leurs semblables - Les opérateurs arithmétiques —
__add__,__mul__,__rmul__et bien d'autres - Le protocole de conteneur —
__len__,__getitem__,__contains__ - Le protocole d'itérateur —
__iter__et__next__ - La valeur de vérité —
__bool__ - Les objets appelables —
__call__ - Le protocole de gestionnaire de contexte —
__enter__et__exit__ __hash__— rendre les objets utilisables comme clés de dictionnaire
Avant de lire ce chapitre, assurez-vous d'être à l'aise avec les classes et objets Python et l'héritage Python. Pour les attributs calculés, consultez @property.
Représentation sous forme de chaîne : __repr__ et __str__
Ces deux méthodes contrôlent la façon dont un objet est converti en chaîne de caractères.
| Méthode | Appelée par | Objectif |
|---|---|---|
__repr__ | repr(), shell interactif | Représentation non ambiguë, destinée aux développeurs |
__str__ | str(), print(), f-strings | Affichage convivial pour les humains |
Si __str__ n'est pas définie, Python se rabat sur __repr__. Il est donc recommandé de toujours définir __repr__ et de définir __str__ uniquement lorsque vous souhaitez un format lisible différent.
class Book:
def __init__(self, title, author, pages):
self.title = title
self.author = author
self.pages = pages
def __repr__(self):
return f"Book(title={self.title!r}, author={self.author!r}, pages={self.pages})"
def __str__(self):
return f'"{self.title}" by {self.author} ({self.pages} pages)'
b = Book("Clean Code", "Robert C. Martin", 431)
print(repr(b)) # Book(title='Clean Code', author='Robert C. Martin', pages=431)
print(str(b)) # "Clean Code" by Robert C. Martin (431 pages)
print(b) # "Clean Code" by Robert C. Martin (431 pages)Le drapeau de conversion !r à l'intérieur d'une f-string appelle repr() sur cette valeur, ce qui entoure les chaînes de guillemets. Cela rend la sortie de __repr__ copiable-collable en tant que code Python.
Astuce : un bon __repr__ vous permet de reconstruire l'objet à partir de sa sortie. Pensez-y comme à eval(repr(obj)) == obj en tant que modèle mental (même si ce n'est pas toujours littéralement vrai).
Opérateurs de comparaison
Les opérateurs de comparaison Python correspondent tous à des méthodes dunder. Définissez-les lorsque vous souhaitez que ==, <, >, <= ou >= comparent vos objets de façon significative.
| Opérateur | Méthode | Méthode réfléchie |
|---|---|---|
== | __eq__ | __eq__ |
!= | __ne__ | __ne__ |
< | __lt__ | __gt__ |
<= | __le__ | __ge__ |
> | __gt__ | __lt__ |
>= | __ge__ | __le__ |
Réfléchie signifie que Python essaie la méthode de l'opérande droit lorsque l'opérande gauche retourne NotImplemented. Par exemple, si a < b appelle a.__lt__(b) et que cela retourne NotImplemented, Python essaie alors la méthode réfléchie : b.__gt__(a).
class Temperature:
def __init__(self, celsius):
self.celsius = celsius
def __eq__(self, other):
if not isinstance(other, Temperature):
return NotImplemented
return self.celsius == other.celsius
def __lt__(self, other):
if not isinstance(other, Temperature):
return NotImplemented
return self.celsius < other.celsius
def __le__(self, other):
if not isinstance(other, Temperature):
return NotImplemented
return self.celsius <= other.celsius
def __repr__(self):
return f"Temperature({self.celsius}°C)"
t1 = Temperature(20)
t2 = Temperature(30)
t3 = Temperature(20)
print(t1 == t3) # True
print(t1 < t2) # True
print(t2 > t1) # True — Python derives __gt__ from __lt__ via reflection
print(t1 <= t3) # TrueRaccourci : si vous souhaitez simplement que les objets soient triables sans vous soucier des six opérateurs individuellement, utilisez le décorateur @functools.total_ordering. Définissez __eq__ et l'un parmi __lt__, __le__, __gt__ ou __ge__, et total_ordering complète automatiquement le reste.
from functools import total_ordering
@total_ordering
class Temperature:
def __init__(self, celsius):
self.celsius = celsius
def __eq__(self, other):
if not isinstance(other, Temperature):
return NotImplemented
return self.celsius == other.celsius
def __lt__(self, other):
if not isinstance(other, Temperature):
return NotImplemented
return self.celsius < other.celsiusOpérateurs arithmétiques
Les dunders arithmétiques permettent à vos objets de fonctionner avec +, -, *, /, //, % et **.
| Expression | Méthode | Notes |
|---|---|---|
a + b | __add__ | |
a - b | __sub__ | |
a * b | __mul__ | |
b * a | __rmul__ | version de droite ; appelée quand b.__mul__(a) retourne NotImplemented |
-a | __neg__ | négation unaire |
abs(a) | __abs__ | |
a += b | __iadd__ | en place ; se rabat sur __add__ si non défini |
Un cas d'utilisation classique est une classe vecteur 2D :
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
return Vector(self.x + other.x, self.y + other.y)
def __sub__(self, other):
return Vector(self.x - other.x, self.y - other.y)
def __mul__(self, scalar):
return Vector(self.x * scalar, self.y * scalar)
def __rmul__(self, scalar): # supports: 3 * v
return self.__mul__(scalar)
def __neg__(self):
return Vector(-self.x, -self.y)
def __abs__(self):
return (self.x ** 2 + self.y ** 2) ** 0.5
def __repr__(self):
return f"Vector({self.x}, {self.y})"
v1 = Vector(1, 2)
v2 = Vector(3, 4)
print(v1 + v2) # Vector(4, 6)
print(v2 - v1) # Vector(2, 2)
print(v1 * 3) # Vector(3, 6)
print(3 * v1) # Vector(3, 6) — uses __rmul__
print(-v1) # Vector(-1, -2)
print(abs(v2)) # 5.0__rmul__ est ce qui permet à 3 * v1 de fonctionner. Lorsque Python évalue 3 * v1, il appelle d'abord int.__mul__(3, v1). La classe entière intégrée ne sait pas comment multiplier un entier par un Vector, donc elle retourne NotImplemented. Python essaie alors la méthode réfléchie : v1.__rmul__(3), qui réussit.
Protocole de conteneur
Implémentez ces méthodes pour que votre classe se comporte comme une séquence ou une collection.
| Méthode | Appelée par | Ce qu'elle permet |
|---|---|---|
__len__ | len(obj) | Longueur du conteneur |
__getitem__ | obj[index] | Accès par index et par slice |
__setitem__ | obj[index] = val | Affectation par index |
__delitem__ | del obj[index] | Suppression par index |
__contains__ | item in obj | Test d'appartenance |
Définir __len__ conjointement avec __getitem__ suffit à rendre votre classe automatiquement itérable — la boucle for de Python appellera __getitem__ avec des indices successifs à partir de 0 jusqu'à obtenir une IndexError.
class WordBag:
def __init__(self, *words):
self._words = list(words)
def __len__(self):
return len(self._words)
def __contains__(self, item):
return item in self._words
def __getitem__(self, index):
return self._words[index]
def __repr__(self):
return f"WordBag({self._words!r})"
bag = WordBag("apple", "banana", "cherry")
print(len(bag)) # 3
print("banana" in bag) # True
print("grape" in bag) # False
print(bag[0]) # apple
print(bag[-1]) # cherry
# __len__ + __getitem__ makes the object iterable automatically
for word in bag:
print(word)
# apple
# banana
# cherryProtocole d'itérateur
Si vous souhaitez un comportement d'itérateur complet (fonctionner avec iter() et next() directement, ou être utilisable dans des endroits qui nécessitent un itérateur plutôt qu'un simple itérable), définissez à la fois __iter__ et __next__ :
__iter__— appelé pariter(obj)et au début d'une bouclefor; doit retourner l'objet itérateur (généralementself).__next__— appelé de façon répétée pour produire la valeur suivante ; doit leverStopIterationquand l'itérateur est épuisé.
class Countdown:
def __init__(self, start):
self.start = start
def __iter__(self):
self.current = self.start
return self
def __next__(self):
if self.current < 0:
raise StopIteration
value = self.current
self.current -= 1
return value
for n in Countdown(3):
print(n)
# 3
# 2
# 1
# 0Pour des modèles d'itération plus puissants — notamment des séquences paresseuses qui produisent des valeurs à la demande — consultez Python Generators et Python Iterators.
Valeur de vérité : __bool__
Python appelle __bool__ lorsqu'un objet est utilisé dans un contexte boolean (une instruction if, une boucle while, not, and, or). Si __bool__ n'est pas défini mais que __len__ l'est, Python utilise len(obj) != 0 comme valeur de vérité. Si aucun des deux n'est défini, l'objet est toujours vrai (truthy).
class Stack:
def __init__(self):
self._data = []
def push(self, item):
self._data.append(item)
def pop(self):
return self._data.pop()
def __len__(self):
return len(self._data)
def __bool__(self):
return len(self._data) > 0
def __repr__(self):
return f"Stack({self._data!r})"
s = Stack()
print(bool(s)) # False — empty stack is falsy
s.push(1)
print(bool(s)) # True
print(len(s)) # 1
if s:
print("stack has items") # stack has itemsCela reflète le comportement des collections intégrées : une liste, un dict ou un ensemble vide est faux (falsy) ; un non-vide est vrai (truthy).
Objets appelables : __call__
Définir __call__ vous permet d'utiliser une instance comme si c'était une fonction. Cela est utile pour les objets qui maintiennent un état entre les appels — ce qu'une simple fonction ne peut pas faire sans une fermeture ou une variable globale.
class Multiplier:
def __init__(self, factor):
self.factor = factor
def __call__(self, value):
return value * self.factor
double = Multiplier(2)
triple = Multiplier(3)
print(double(5)) # 10
print(triple(5)) # 15
print(callable(double)) # Truedouble et triple sont des objets ordinaires, mais vous les appelez avec () tout comme des fonctions. La fonction intégrée callable() retourne True pour tout objet possédant __call__.
Ce modèle est courant dans les frameworks de machine learning (couches, fonctions de perte) et dans les fabriques de décorateurs. Consultez Python Decorators pour un cas d'utilisation étroitement lié.
Protocole de gestionnaire de contexte : __enter__ et __exit__
L'instruction with est le moyen qu'a Python de configurer et démanteler une ressource de manière fiable — même en cas d'exception. Tout objet qui définit __enter__ et __exit__ peut être utilisé comme gestionnaire de contexte.
__enter__(self)— s'exécute au démarrage du blocwith; sa valeur de retour est liée à la variableas.__exit__(self, exc_type, exc_val, exc_tb)— s'exécute à la sortie du bloc, que ce soit normalement ou via une exception. RetournezTruepour supprimer l'exception ; retournezFalse(ouNone) pour la laisser se propager.
class ManagedFile:
def __init__(self, path, mode="r"):
self.path = path
self.mode = mode
self._file = None
def __enter__(self):
self._file = open(self.path, self.mode)
return self._file # the value bound to the "as" variable
def __exit__(self, exc_type, exc_val, exc_tb):
if self._file:
self._file.close()
return False # do not suppress exceptions
with ManagedFile("/etc/hostname") as f:
content = f.read()
# The file is guaranteed to be closed here, even if an exception occurred inside the block.Le décorateur contextlib.contextmanager de la bibliothèque standard vous permet d'écrire la même logique sous forme de fonction génératrice — une alternative plus légère pour les cas simples. Consultez Python with Statement pour une couverture complète.
Hachage : __hash__
Python utilise __hash__ pour placer les objets dans des ensembles et des dictionnaires. Le __hash__ par défaut est basé sur l'adresse mémoire de l'objet (identité). Lorsque vous surchargez __eq__, Python définit automatiquement __hash__ à None, rendant vos objets non hachables — vous devez définir __hash__ explicitement si vous souhaitez qu'ils fonctionnent toujours dans des ensembles ou comme clés de dict.
La règle : les objets qui sont égaux en comparaison doivent avoir le même hash.
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __eq__(self, other):
if not isinstance(other, Point):
return NotImplemented
return self.x == other.x and self.y == other.y
def __hash__(self):
return hash((self.x, self.y)) # hash of an immutable tuple
def __repr__(self):
return f"Point({self.x}, {self.y})"
p1 = Point(1, 2)
p2 = Point(1, 2)
p3 = Point(3, 4)
print(p1 == p2) # True
print(p1 is p2) # False — different objects in memory
print(hash(p1) == hash(p2)) # True
seen = {p1, p2, p3}
print(len(seen)) # 2 — p1 and p2 are equal, so only one copy kept
print(p1 in seen) # TrueSi votre classe est mutable (ses champs peuvent changer après la création), ne définissez pas __hash__. Les objets mutables ne doivent pas être hachables car modifier leurs champs changerait leur hash, corrompant tout ensemble ou dict qui les contient déjà.
Tableau récapitulatif
| Catégorie | Méthode | Déclenchée par |
|---|---|---|
| Représentation | __repr__ | repr(obj), shell interactif |
| Représentation | __str__ | str(obj), print(obj), f-strings |
| Comparaison | __eq__, __ne__ | ==, != |
| Comparaison | __lt__, __le__, __gt__, __ge__ | <, <=, >, >= |
| Arithmétique | __add__, __sub__, __mul__ | +, -, * |
| Arithmétique | __rmul__, __radd__, … | formes réfléchies de droite |
| Arithmétique | __neg__, __abs__ | - unaire, abs() |
| Conteneur | __len__ | len(obj) |
| Conteneur | __getitem__, __setitem__, __delitem__ | obj[i], obj[i] = v, del obj[i] |
| Conteneur | __contains__ | item in obj |
| Itérateur | __iter__ | iter(obj), boucle for |
| Itérateur | __next__ | next(obj) |
| Valeur de vérité | __bool__ | bool(obj), if obj: |
| Appelable | __call__ | obj(args) |
| Gestionnaire de contexte | __enter__, __exit__ | with obj as x: |
| Hachage | __hash__ | hash(obj), clés de dict, ensembles |
Quand utiliser les méthodes magiques
- Utilisez-les lorsque votre classe représente un type valeur (un point, un vecteur, un montant monétaire, une plage de dates) — la surcharge des opérateurs et des comparaisons rend la classe naturelle à utiliser.
- Utilisez-les lorsque votre classe encapsule une ressource (un fichier, une connexion à une base de données, un socket réseau) —
__enter__/__exit__garantit que la ressource est toujours libérée. - Utilisez-les lorsque votre classe est une collection personnalisée — les protocoles de conteneur et d'itérateur lui permettent de fonctionner avec
for,in,len()et les compréhensions de liste. - Évitez-les pour les classes d'application ordinaires qui ne sont ni des types valeur ni des conteneurs. Surcharger
+sur une classeUserserait source de confusion.
Pour des modèles POO plus avancés, consultez Python Abstract Classes, Python Encapsulation et Python Polymorphism.