Question 1Quand result = a + b s'exécute, quelle méthode Python appelle-t-il en coulisses ?
Méthodes spéciales — Apprendre à ta classe comment se comportent + et print
Apprends les méthodes spéciales de Python (méthodes dunder). Personnalise + avec __add__, contrôle la sortie de print avec __str__, et redéfinis == avec __eq__ — le tout avec des schémas.
La dernière fois on a trié les méthodes d'instance, de classe et statiques. Cette fois on couvre un autre genre — les méthodes spéciales (alias méthodes dunder).
Que sont les méthodes spéciales ? — « Méthodes dunder »
Les méthodes comme __init__ et __str__ — des noms encadrés par deux underscores de chaque côté — sont appelées méthodes spéciales. Comme elles sont entre double underscores, on les appelle aussi méthodes dunder.
Le point clé sur les méthodes spéciales, c'est que tu ne les appelles pas directement. Python les appelle automatiquement quand tu effectues certaines opérations. Écris v1 + v2 et Python appelle v1.__add__(v2) en coulisses. Écris print(p) et p.__str__() est appelée. Écris a == b et a.__eq__(b) s'exécute.
+, ==, et compagnie.__add__ — Définir l'opérateur +
Prenons + comme exemple évident. Suppose que tu as une classe Money pour des montants en euros, et que tu veux que Money(300) + Money(500) te donne Money(800). Essaie de les additionner d'emblée et Python lèvera une TypeError en se plaignant qu'il ne sait pas comment additionner Money à Money.
La façon d'apprendre à Python à les additionner, c'est la méthode __add__. Définis def __add__(self, other): et renvoie une nouvelle instance construite à partir de self (côté gauche) et other (côté droit). Maintenant + fonctionne sur ta classe.
class Money:
def __init__(self, amount):
self.amount = amount
def __add__(self, other): # appelé quand on écrit +
return Money(self.amount + other.amount)
wallet = Money(300)
payment = Money(500)
total = wallet + payment # vraiment wallet.__add__(payment)
print(total.amount) # 800
+ et Python appelle self.__add__(other). self est l'opérande gauche, other est celui de droite. Quelle que soit la nouvelle instance que tu renvoies, elle devient le résultat de +.__str__ et __repr__ — Deux types de représentation en chaîne
Ensuite : les méthodes spéciales appelées quand une instance est convertie en chaîne. Il y en a deux, avec des objectifs différents.
- __str__ — appelée par print(p) et str(p). Une chaîne lisible, orientée utilisateur.
- __repr__ — appelée dans le REPL ou pour le débogage. Une chaîne proche du code, détaillée.
Sans surcharge, print(user) affiche quelque chose comme <__main__.User object at 0x...> — assez inutile. Définis __str__ pour un affichage propre, et définis aussi __repr__ pour que le débogage te montre le type et le contenu d'un coup d'œil.
class User:
def __init__(self, name, age):
self.name = name
self.age = age
def __str__(self): # pour print() / str()
return f"{self.name} ({self.age} ans)"
def __repr__(self): # pour le débogage
return f"User(name={self.name!r}, age={self.age})"
u = User("Alice", 30)
print(u) # Alice (30 ans) <- __str__
print(repr(u)) # User(name='Alice', age=30) <- __repr__
- Appelée par
print(u)etstr(u) - Objectif : une chaîne à lire pour les utilisateurs finaux
- Exemple :
Alice (30 ans)
- Appelée par
repr(u)et le REPL interactif - Sert de repli quand __str__ est absente lors d'un print
- Objectif : une chaîne de débogage montrant type et contenu
- Exemple :
User(name='Alice', age=30)
__repr__ — ça facilite énormément le débogage.__eq__ — Définir ==
Maintenant l'égalité. Quand tu écris a == b, Python appelle a.__eq__(b) en interne. Si ta classe ne définit pas __eq__, le défaut est « sont-ils le même objet en mémoire ? » (une comparaison d'id).
Dis que tu as une classe Coupon et que tu veux que deux coupons avec le même code comptent comme le même coupon — même si les valeurs de remise diffèrent. C'est une égalité « niveau métier », et __eq__ est l'endroit où tu la formules.
== est une vérification d'id — même contenu mais instances séparées renvoient toujours False. Avec __eq__, tu peux comparer par contenu (code).class Coupon:
def __init__(self, code, discount):
self.code = code
self.discount = discount
def __eq__(self, other):
return self.code == other.code # même code = même coupon
c1 = Coupon("SPRING10", 0.10)
c2 = Coupon("SPRING10", 0.20) # remise différente, même code
c3 = Coupon("SUMMER15", 0.15)
print(c1 == c2) # True (codes identiques)
print(c1 == c3) # False (codes différents)
Que se passe-t-il sans __eq__ ?
Si tu ne définis pas __eq__, c1 == c2 vérifie s'ils sont le même objet en mémoire (pointant littéralement vers la même boîte). Même si chaque attribut correspond parfaitement, deux instances construites séparément vivent à des adresses mémoire différentes et le résultat est False. Quand tu veux « égal par contenu », définis __eq__ toi-même.
Autres méthodes spéciales courantes — __call__ / __len__ / __bool__
Au-delà des quatre qu'on a couvertes, plusieurs autres méthodes spéciales reviennent souvent. Trois à connaître tout de suite :
- __call__ — rend une instance appelable comme une fonction (syntaxe obj(...))
- __len__ — définit ce que len(obj) renvoie
- __bool__ — définit comment l'instance s'évalue en valeur de vérité dans if obj: ou bool(obj)
Chacune apprend une opération native — appel de fonction, len(), le test de véracité de if — à ta classe personnalisée. Un Logger qui accumule des entrées de log est un excellent exemple pour mettre les trois sur une même classe.
class Logger:
def __init__(self, name):
self.name = name
self.log = []
def __call__(self, message): # logger("...") fonctionne
self.log.append(message)
return f"[{self.name}] {message}"
def __len__(self): # pour len(logger)
return len(self.log)
def __bool__(self): # pour if logger:
return len(self.log) > 0
app = Logger("app")
print(app("Application démarrée")) # [app] Application démarrée
print(app("Connexion réussie")) # [app] Connexion réussie
print(len(app)) # 2
if app:
print("Il y a des logs") # Il y a des logs
logger(...), len(logger), if logger: — est apprise à ta classe personnalisée via trois dunders correspondants.Et si tu ne définis pas __bool__ ?
Quand __bool__ est absente, Python retombe sur __len__. Une longueur de 0 devient False ; tout le reste devient True. Si les deux sont absentes, les instances sont toujours vraies, peu importe leur état. C'est la même règle qui fait que les listes et chaînes vides s'évaluent à False.
Il y en a beaucoup d'autres — __hash__ (pour utilisation comme clé set/dict), __lt__ / __gt__ (les comparaisons < / >), __getitem__ (la syntaxe obj[key]), __iter__ (l'itération for x in obj:), et d'autres. Tu n'as pas besoin de toutes les mémoriser. Garde juste cette carte mentale : « si tu veux apprendre une opération native à ta classe, il y a probablement un dunder correspondant. » Puis cherche-le quand tu en as besoin.
| Méthode spéciale | Syntaxe | Quand elle est appelée |
|---|---|---|
| __init__ | Money(300) | À la création d'instance |
| __add__ | a + b | Opérateur + |
| __str__ | print(p) / str(p) | Chaîne orientée utilisateur |
| __repr__ | repr(p) / affichage REPL | Chaîne orientée développeur |
| __eq__ | a == b | Comparaison d'égalité |
| __call__ | obj(...) | Appel de style fonction |
| __len__ | len(obj) | Longueur |
| __bool__ | if obj: / bool(obj) | Véracité |
Vérification des connaissances
Répondez à chaque question une par une.
Question 2Quelle description de __str__ et __repr__ est la plus précise ?
Question 3Si une classe ne définit pas __eq__, que compare a == b ? (Les deux sont des instances de la même classe.)