__add__ e __iadd__ definen dos comportamientos distintos para la adición en Python: uno crea un nuevo objeto, el otro modifica el existente. Esta distinción tiene consecuencias reales sobre los alias y las referencias compartidas, y sorprende incluso a desarrolladores experimentados.
add: la adición que crea
__add__ es invocada por el operador +. Debe retornar un nuevo objeto y dejar los operandos sin cambios.
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 # nuevo Vector(4, 6)
print(a.x, a.y) # 1 2 — a no cambia
print(id(a) == id(c)) # False — objetos distintos
a + b llama a a.__add__(b). Si __add__ no está definido en a o retorna NotImplemented, Python intenta b.__radd__(a).
iadd: la adición que muta
__iadd__ es invocada por el operador +=. Modifica el objeto en lugar y retorna 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 # obligatorio
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 — mismo objeto
Retornar self es no negociable: Python reasigna el resultado de __iadd__ a la variable izquierda. Olvidar el return self equivale a escribir a = None.
La trampa de los alias
La distinción copia/referencia se vuelve crítica cuando múltiples variables apuntan al mismo objeto.
a = Vector(1, 2)
alias = a # alias y a apuntan al mismo objeto
# Con __add__: a + b crea un nuevo Vector
a = a + Vector(3, 4)
print(id(a) == id(alias)) # False — a apunta a un nuevo objeto
print(alias.x, alias.y) # 1 2 — alias sigue apuntando al original
# Con __iadd__: a += b muta en lugar
a = Vector(1, 2)
alias = a
a += Vector(3, 4)
print(id(a) == id(alias)) # True — mismo objeto
print(alias.x, alias.y) # 4 6 — alias ve el cambio
Cuando un objeto se comparte entre múltiples referencias (atributo de clase, argumento de función, elemento en una lista), += con __iadd__ propaga la modificación a todos los puntos de acceso.
El fallback silencioso
Si __iadd__ no está definido, Python usa __add__ como fallback para +=. La operación crea entonces un nuevo objeto en lugar de mutar el existente.
class Counter:
def __init__(self, n):
self.n = n
def __add__(self, other):
return Counter(self.n + other.n)
# sin __iadd__
a = Counter(5)
original_id = id(a)
a += Counter(3)
print(id(a) == original_id) # False — nuevo objeto, __add__ usado como fallback
Este comportamiento es correcto pero puede sorprender: a += b actúa como a = a + b en lugar de como una mutación en lugar.
Tipos inmutables: += siempre por copia
Los tipos inmutables (int, str, tuple) no tienen __iadd__. El operador += siempre crea un nuevo objeto.
a = "hello"
original_id = id(a)
a += " world"
print(id(a) == original_id) # False — nueva cadena
x = 10
ref = x
x += 1
print(ref) # 10 — ref sigue apuntando al int original
Tipos mutables: += muta en lugar
Los tipos mutables como list tienen un __iadd__ que extiende la lista en lugar mediante extend(). La diferencia con + no es cosmética.
a = [1, 2]
alias = a
a += [3] # llama a list.__iadd__, muta en lugar
print(alias) # [1, 2, 3] — alias ve el cambio
a = [1, 2]
alias = a
a = a + [3] # llama a list.__add__, crea una nueva lista
print(alias) # [1, 2] — alias sigue apuntando a la lista antigua
Cuándo implementar cada uno
| Caso | Recomendación |
|---|---|
| Objeto inmutable por diseño | Solo __add__ |
| Objeto mutable con estado interno | __add__ + __iadd__ |
__iadd__ sin __add__ | Desaconsejado: + queda inutilizable |
Si el objeto es mutable y la modificación en lugar es semánticamente correcta, implementar __iadd__ evita copias innecesarias y hace explícita la intención.
