W3docs

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 PythonDunder appelé
str(obj)obj.__str__()
len(obj)obj.__len__()
a + ba.__add__(b)
a == ba.__eq__(b)
item in objobj.__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éthodeAppelée parObjectif
__repr__repr(), shell interactifReprésentation non ambiguë, destinée aux développeurs
__str__str(), print(), f-stringsAffichage 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érateurMéthodeMé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)  # True

Raccourci : 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.celsius

Opérateurs arithmétiques

Les dunders arithmétiques permettent à vos objets de fonctionner avec +, -, *, /, //, % et **.

ExpressionMéthodeNotes
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éthodeAppelée parCe qu'elle permet
__len__len(obj)Longueur du conteneur
__getitem__obj[index]Accès par index et par slice
__setitem__obj[index] = valAffectation par index
__delitem__del obj[index]Suppression par index
__contains__item in objTest 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
# cherry

Protocole 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é par iter(obj) et au début d'une boucle for ; doit retourner l'objet itérateur (généralement self).
  • __next__ — appelé de façon répétée pour produire la valeur suivante ; doit lever StopIteration quand 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
# 0

Pour 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 items

Cela 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))  # True

double 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 bloc with ; sa valeur de retour est liée à la variable as.
  • __exit__(self, exc_type, exc_val, exc_tb) — s'exécute à la sortie du bloc, que ce soit normalement ou via une exception. Retournez True pour supprimer l'exception ; retournez False (ou None) 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)  # True

Si 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égorieMéthodeDé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 classe User serait source de confusion.

Pour des modèles POO plus avancés, consultez Python Abstract Classes, Python Encapsulation et Python Polymorphism.

Pratique

Pratique
Which dunder method does Python call when you use an object in a boolean context such as 'if obj:'?
Which dunder method does Python call when you use an object in a boolean context such as 'if obj:'?
Was this page helpful?