__add__ et __iadd__ définissent deux comportements distincts pour l’addition en Python : l’un crée un nouvel objet, l’autre modifie l’existant. Cette distinction a des conséquences réelles sur les alias et les références partagées, et elle réserve des surprises même aux développeurs expérimentés.
add : l’addition qui crée
__add__ est appelée par l’opérateur +. Elle doit retourner un nouvel objet et laisser les opérandes inchangés.
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)
a = Vector(1, 2)
b = Vector(3, 4)
c = a + b # nouvel objet Vector(4, 6)
print(a.x, a.y) # 1 2 — a est inchangé
print(id(a) == id(c)) # False — objets distincts
a + b appelle a.__add__(b). Si __add__ n’est pas défini sur a ou retourne NotImplemented, Python essaie b.__radd__(a).
iadd : l’addition qui modifie
__iadd__ est appelée par l’opérateur +=. Elle modifie l’objet en place et retourne self.
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __iadd__(self, other):
self.x += other.x
self.y += other.y
return self # obligatoire
a = Vector(1, 2)
b = Vector(3, 4)
original_id = id(a)
a += b
print(a.x, a.y) # 4 6
print(id(a) == original_id) # True — même objet
Retourner self est non négociable : Python réassigne le résultat de __iadd__ à la variable gauche. Oublier le return self revient à écrire a = None.
Le piège des alias
La distinction copie/référence devient critique quand plusieurs variables pointent vers le même objet.
a = Vector(1, 2)
alias = a # alias et a pointent vers le même objet
# Avec __add__ : a + b crée un nouveau Vector
a = a + Vector(3, 4)
print(id(a) == id(alias)) # False — a pointe vers un nouvel objet
print(alias.x, alias.y) # 1 2 — alias pointe toujours vers l'original
# Avec __iadd__ : a += b modifie l'objet en place
a = Vector(1, 2)
alias = a
a += Vector(3, 4)
print(id(a) == id(alias)) # True — même objet
print(alias.x, alias.y) # 4 6 — alias voit le changement
Dans un contexte où l’objet est partagé entre plusieurs références (attribut de classe, argument passé à une fonction, élément dans une liste), += avec __iadd__ propage la modification à tous les points d’accès.
Le fallback silencieux
Si __iadd__ n’est pas défini, Python utilise __add__ comme fallback pour +=. L’opération crée alors un nouvel objet au lieu de modifier l’existant.
class Counter:
def __init__(self, n):
self.n = n
def __add__(self, other):
return Counter(self.n + other.n)
# pas de __iadd__
a = Counter(5)
original_id = id(a)
a += Counter(3)
print(id(a) == original_id) # False — nouvel objet, __add__ utilisé en fallback
Ce comportement est correct mais peut surprendre : a += b se comporte comme a = a + b plutôt que comme une modification en place.
Types immutables : += toujours par copie
Les types immutables (int, str, tuple) n’ont pas de __iadd__. L’opérateur += crée systématiquement un nouvel objet.
a = "hello"
original_id = id(a)
a += " world"
print(id(a) == original_id) # False — nouvelle chaîne
x = 10
ref = x
x += 1
print(ref) # 10 — ref pointe toujours vers l'ancien int
Types mutables : += modifie en place
Les types mutables comme list ont un __iadd__ qui étend la liste en place via extend(). La différence avec + n’est pas cosmétique.
a = [1, 2]
alias = a
a += [3] # appelle list.__iadd__, modifie en place
print(alias) # [1, 2, 3] — alias voit le changement
a = [1, 2]
alias = a
a = a + [3] # appelle list.__add__, crée une nouvelle liste
print(alias) # [1, 2] — alias pointe toujours vers l'ancienne liste
Quand implémenter chacun
| Cas | Recommandation |
|---|---|
| Objet immutable par design | __add__ uniquement |
| Objet mutable avec état interne | __add__ + __iadd__ |
__iadd__ sans __add__ | Déconseillé : + devient inutilisable |
Si l’objet est mutable et que la modification en place est sémantiquement correcte, implémenter __iadd__ évite des copies inutiles et rend l’intention explicite.
