Sobrecarga de operadores en Python — teoría con Vector2D: __eq__, __add__, __str__, operadores reflejados y @total_ordering (y los dos vectores iguales que Python decía que no lo eran)
Preparando un práctico de FP2 escribí una clase Vector2D, creé dos vectores con las mismas coordenadas y, para comprobar que todo iba bien, los comparé. Python me contestó que no eran iguales. Después intenté sumarlos:
class Vector2D:
def __init__(self, x, y):
self.x = x
self.y = y
v1 = Vector2D(3, 4)
v2 = Vector2D(3, 4)
print(v1 == v2) # mismas coordenadas…
print(v1 is v2)
print(v1 + v2)
Salida:
False
False
Traceback (most recent call last):
File "gancho.py", line 11, in <module>
print(v1 + v2)
~~~^~~~
TypeError: unsupported operand type(s) for +: 'Vector2D' and 'Vector2D'
¿Cómo que (3, 4) no es igual a (3, 4)? Para Python no lo es: sin instrucciones, == compara si los dos nombres apuntan al mismo objeto (como is), y + no sabe qué significa sumar dos objetos de una clase que se acaba de inventar. Hay que enseñárselo, y eso es la sobrecarga de operadores: escribir en tu clase los métodos mágicos (__eq__, __add__, __lt__…) que Python llama cuando usas ==, + o < con tus objetos.
Es lo que se prometió en la teoría de clases y en la práctica de excepciones: __add__ y __eq__ llegaban «en el tema de sobrecarga». Y viene justo después de herencia en Python porque, en el fondo, sobrecargar un operador es sustituir un método que tu clase hereda de object.
En este artículo construimos Vector2D paso a paso y vemos todo lo que entra en FP2: __str__ frente a __repr__, __eq__ y el __hash__ que desaparece, @total_ordering, operadores aritméticos, NotImplemented, operadores reflejados (__radd__, __rmul__, __rsub__), operadores unarios, += con y sin __iadd__ y por qué en Python no existe la sobrecarga de funciones.
Tabla de Contenidos
—
¿Qué es la sobrecarga de operadores?
En Python los operadores ya están sobrecargados de serie: + suma números, pero también une cadenas y listas. El mismo símbolo hace cosas distintas según el tipo. La razón es que un operador es una llamada a un método:
print(3 + 5) # suma de enteros
print('Hola' + ' mundo') # concatenación de cadenas
print([1, 2] + [3, 4]) # concatenación de listas
print((3).__add__(5)) # lo que Python hace por dentro con 3 + 5
print('Hola'.__add__(' mundo'))
print('__add__' in dir(int), '__add__' in dir(str), '__add__' in dir(dict))
Salida:
8 Hola mundo [1, 2, 3, 4] 8 Hola mundo True True False
3 + 5 es, por dentro, (3).__add__(5): el método __add__ del entero 3. Las cadenas tienen su propio __add__, que concatena. Los diccionarios no tienen __add__, y por eso {} + {} da TypeError. Sobrecargar un operador en tu clase es escribir ese método: si Vector2D tiene __add__, v + w funciona.
| Escribes | Python llama | Método mágico |
|---|---|---|
a + b | a.__add__(b) | suma |
a - b | a.__sub__(b) | resta |
a * b | a.__mul__(b) | multiplicación |
a / b | a.__truediv__(b) | división |
a == b | a.__eq__(b) | igualdad |
a < b | a.__lt__(b) | menor que |
-a | a.__neg__() | negación |
abs(a) | a.__abs__() | valor absoluto |
len(a) | a.__len__() | longitud |
x in a | a.__contains__(x) | pertenencia |
print(a) | a.__str__() | texto para personas |
En los apuntes de FP2 lo verás comparado con Java, y ahí hay una diferencia que cae en el test: Java no permite sobrecargar operadores. Para comparar objetos en Java se implementa equals o la interfaz Comparable; en Python, __eq__ y __lt__.
—
La clase Vector2D: punto de partida
Un vector del plano tiene dos coordenadas y es perfecto para el tema: se suma, se resta, se multiplica por un número, tiene longitud (módulo) y se compara. Lo primero que conviene darle es una forma de imprimirse, porque si no, cada prueba saca <__main__.Vector2D object at 0x…>. Para eso hay dos métodos, y confundirlos es uno de los fallos del tema:
class Vector2D:
def __init__(self, x, y):
self.x = x
self.y = y
def __str__(self): # para personas: print(), str(), f-strings
return f'({self.x}, {self.y})'
def __repr__(self): # para programadores: listas, consola, depurar
return f'Vector2D({self.x!r}, {self.y!r})'
v = Vector2D(3, 4)
print(v)
print(str(v), repr(v))
print([v, Vector2D(1, 2)]) # una lista muestra el repr de cada elemento
print(f'v = {v} -> {v!r}')
Salida:
(3, 4) (3, 4) Vector2D(3, 4) [Vector2D(3, 4), Vector2D(1, 2)] v = (3, 4) -> Vector2D(3, 4)
__str__es el texto para personas: lo usanprint,str()y las f-strings normales. Aquí,(3, 4).__repr__es el texto para programadores: lo usan la consola,repr(),!ren las f-strings y, sobre todo, las listas, tuplas y diccionarios cuando se imprimen. La costumbre es que parezca el código que crea el objeto:Vector2D(3, 4).
Fíjate en la tercera línea: al imprimir una lista, Python usa el repr de cada elemento, no el str. Por eso, si solo escribes uno, que sea __repr__: cuando falta __str__, print usa __repr__; al revés, no.
Escribir solo __str__ y extrañarse de que print([v, w]) siga sacando <__main__.Vector2D object at 0x…>. Las colecciones usan __repr__.
—
Operadores de comparación
__eq__ y NotImplemented
Para que == compare coordenadas, Vector2D necesita __eq__. Recibe self (el de la izquierda) y el otro objeto (el de la derecha) y devuelve True o False:
class Vector2D:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f'Vector2D({self.x}, {self.y})'
def __eq__(self, otro):
if not isinstance(otro, Vector2D):
return NotImplemented # «no sé compararme con eso»
return self.x == otro.x and self.y == otro.y
v = Vector2D(3, 4)
print(v == Vector2D(3, 4), v is Vector2D(3, 4))
print(v != Vector2D(1, 2)) # __ne__ sale solo de __eq__
print(v == (3, 4), v == 'hola') # tipos distintos: False, sin error
print(Vector2D(3, 4) in [Vector2D(1, 2), Vector2D(3, 4)])
print(Vector2D.__hash__) # definir __eq__ quita el hash
try:
print({v})
except TypeError as err:
print('TypeError:', err)
Salida:
True False True False False True None TypeError: unhashable type: 'Vector2D'
Lo importante está en la primera línea de __eq__: si el otro no es un Vector2D, devolvemos NotImplemented. No es False ni un error: es la forma de decirle a Python «yo no sé compararme con eso». Python entonces le pregunta al otro objeto y, si tampoco sabe, decide que no son iguales. Por eso v == (3, 4) y v == 'hola' dan False sin explotar.
¿Y si no comprobamos el tipo?
class Vector2D:
def __init__(self, x, y):
self.x = x
self.y = y
def __eq__(self, otro): # sin comprobar el tipo
return self.x == otro.x and self.y == otro.y
v = Vector2D(3, 4)
print(v == Vector2D(3, 4))
print(v == 5)
Salida:
True
Traceback (most recent call last):
File "eq_mal.py", line 12, in <module>
print(v == 5)
^^^^^^
File "eq_mal.py", line 7, in __eq__
return self.x == otro.x and self.y == otro.y
^^^^^^
AttributeError: 'int' object has no attribute 'x'
Con otro vector funciona, pero v == 5 intenta leer 5.x y revienta con AttributeError. Comparar con == nunca debería lanzar un error: por eso el isinstance va siempre lo primero.
Gratis con __eq__: Python crea != como su negación (lo hace object, no hace falta __ne__), y también funcionan in en listas, list.count, list.index y list.remove, que comparan con ==. Fíjate en el True de la cuarta línea: in encontró el Vector2D(3, 4) de la lista.
El __hash__ que desaparece
La salida tiene una sorpresa al final: Vector2D.__hash__ vale None y meter el vector en un conjunto da TypeError: unhashable type. Al definir __eq__, Python quita el __hash__ heredado de object, porque el de object se basa en la identidad y ya no encajaría con tu igualdad. Si quieres usar tus objetos en un set o como claves de un diccionario, escribe un __hash__ coherente con __eq__: dos objetos iguales deben tener el mismo hash. La forma fácil es hacer el hash de una tupla con los mismos datos que compara __eq__:
class Vector2D:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f'Vector2D({self.x}, {self.y})'
def __eq__(self, otro):
if not isinstance(otro, Vector2D):
return NotImplemented
return (self.x, self.y) == (otro.x, otro.y)
def __hash__(self): # iguales -> mismo hash
return hash((self.x, self.y))
puntos = {Vector2D(3, 4), Vector2D(3, 4), Vector2D(1, 2)}
print(len(puntos), Vector2D(3, 4) in puntos)
Salida:
2 True
El conjunto se queda con 2 elementos: los dos Vector2D(3, 4) son iguales y tienen el mismo hash, así que cuentan como uno.
Calcular el hash con datos que pueden cambiar (un objeto mutable dentro de un conjunto al que luego le cambias x). El conjunto lo guardó en el «cajón» del hash viejo y ya no lo encuentra. Usa __hash__ solo en objetos que no vas a modificar mientras estén dentro.
—
@functools.total_ordering: el atajo que más confunde
Para comparar con <, <=, > y >= harían falta cuatro métodos (__lt__, __le__, __gt__, __ge__). El decorador @total_ordering del módulo functools te deja escribir __eq__ y uno de los cuatro, y genera el resto. Ordenamos los vectores por su longitud (el módulo):
from functools import total_ordering
import math
@total_ordering # completa <=, > y >= a partir de == y <
class Vector2D:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f'Vector2D({self.x}, {self.y})'
def modulo(self):
return math.hypot(self.x, self.y) # raíz de x² + y²
def __eq__(self, otro):
if not isinstance(otro, Vector2D):
return NotImplemented
return (self.x, self.y) == (otro.x, otro.y)
def __lt__(self, otro): # «menor» = más corto
if not isinstance(otro, Vector2D):
return NotImplemented
return self.modulo() < otro.modulo()
a, b, c = Vector2D(3, 4), Vector2D(1, 2), Vector2D(0, 6)
print(a < c, a > b, a <= Vector2D(4, 3), c >= a)
print(sorted([a, b, c]))
print(max([a, b, c]), min([a, b, c]))
print([m for m in ('__le__', '__gt__', '__ge__') if m in Vector2D.__dict__])
Salida:
True True False True [Vector2D(1, 2), Vector2D(3, 4), Vector2D(0, 6)] Vector2D(0, 6) Vector2D(1, 2) ['__le__', '__gt__', '__ge__']
Solo hay __eq__ y __lt__, y sin embargo funcionan >, <= y >=, sorted, max y min. La última línea lo confirma: el decorador añadió __le__, __gt__ y __ge__ a la clase. El dibujo resume quién escribe cada método:
Y ahora la trampa: a <= Vector2D(4, 3) es False. Los dos miden 5, pero no son iguales (tienen coordenadas distintas), así que ni < ni == son ciertos, y <= se construye con esas dos piezas. Si tu < y tu == no miran lo mismo, el orden sale raro. Con números, fracciones o dinero no pasa, porque «igual» y «ni menor ni mayor» coinciden.
Para el examen: @total_ordering necesita __eq__ + uno de __lt__, __le__, __gt__, __ge__. Lo más natural es __eq__ y __lt__. Y sorted() y list.sort() solo necesitan __lt__, incluso sin decorador.
—
Operadores aritméticos binarios
Un operador binario tiene dos operandos: self a la izquierda y el otro a la derecha. Cada método devuelve un objeto nuevo y no toca los originales:
class Vector2D:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f'Vector2D({self.x}, {self.y})'
def __add__(self, otro): # v + w
if isinstance(otro, Vector2D):
return Vector2D(self.x + otro.x, self.y + otro.y)
return NotImplemented
def __sub__(self, otro): # v - w
if isinstance(otro, Vector2D):
return Vector2D(self.x - otro.x, self.y - otro.y)
return NotImplemented
def __mul__(self, otro): # v * número o v * w (producto escalar)
if isinstance(otro, (int, float)):
return Vector2D(self.x * otro, self.y * otro)
if isinstance(otro, Vector2D):
return self.x * otro.x + self.y * otro.y
return NotImplemented
def __truediv__(self, numero): # v / número
if isinstance(numero, (int, float)):
return Vector2D(self.x / numero, self.y / numero)
return NotImplemented
v, w = Vector2D(3, 4), Vector2D(1, 2)
print(v + w, v - w)
print(v * 2, v * w, v / 2)
print(v + w + Vector2D(10, 10)) # (v + w) + …: cada + devuelve un Vector2D
print(v, w) # los originales no cambian
try:
print(v + 2)
except TypeError as err:
print('TypeError:', err)
Salida:
Vector2D(4, 6) Vector2D(2, 2) Vector2D(6, 8) 11 Vector2D(1.5, 2.0) Vector2D(14, 16) Vector2D(3, 4) Vector2D(1, 2) TypeError: unsupported operand type(s) for +: 'Vector2D' and 'int'
Algunas decisiones que conviene ver:
v * 2yv * wusan el mismo__mul__, que mira el tipo del otro: con un número escala el vector; con otro vector devuelve el producto escalar, 3 · 1 + 4 · 2 = 11 (un número, no un vector).v + w + Vector2D(10, 10)funciona porquev + wdevuelve unVector2D, y a ese se le suma el siguiente: (4, 6) + (10, 10) = (14, 16).vywsiguen valiendo lo mismo al final: los operadores aritméticos crean objetos nuevos, igual que3 + 5no cambia el 3.v + 2acaba en unTypeErrorcon un mensaje claro. Es el efecto dereturn NotImplemented: Python prueba con el otro operando, tampoco sabe, y lanza el error él mismo.
Lanzar tú el TypeError (o devolver None, False o un texto) cuando el tipo no encaja. Si lanzas el error, le quitas a Python la oportunidad de preguntar al otro objeto; si devuelves None, v + 'hola' «funciona» y devuelve None sin avisar. Lo verás en el «¿Qué falla aquí?» de los ejercicios.
—
Operadores unarios
Los unarios actúan sobre un solo operando: -v llama a __neg__, +v a __pos__ y abs(v) a __abs__. Y uno muy útil que no parece un operador: __bool__, que decide si el objeto es «verdadero» en un if.
class Vector2D:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f'Vector2D({self.x}, {self.y})'
def __neg__(self): # -v
return Vector2D(-self.x, -self.y)
def __abs__(self): # abs(v): el módulo
return (self.x ** 2 + self.y ** 2) ** 0.5
def __bool__(self): # if v: … (el vector cero es «falso»)
return self.x != 0 or self.y != 0
v = Vector2D(3, 4)
print(-v, abs(v), abs(-v))
for u in [v, Vector2D(0, 0)]:
print(u, 'es verdadero' if u else 'es falso')
Salida:
Vector2D(-3, -4) 5.0 5.0 Vector2D(3, 4) es verdadero Vector2D(0, 0) es falso
abs(v) es el módulo: la raíz de 3² + 4² = 25, es decir, 5,0. Y el vector cero es «falso», como el número 0 o la lista vacía. Si una clase no tiene __bool__ pero sí __len__, Python usa len(obj) != 0; si no tiene ninguno, todo objeto es verdadero.
—
Operadores reflejados: el concepto que más confunde
v * 2 funciona. ¿Y 2 * v? Ahí el que está a la izquierda es el entero 2, y int no sabe multiplicar vectores. Lo mismo le pasa a sum(), que empieza sumando 0 + primer_elemento:
class Vector2D:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f'Vector2D({self.x}, {self.y})'
def __add__(self, otro):
if isinstance(otro, Vector2D):
return Vector2D(self.x + otro.x, self.y + otro.y)
return NotImplemented
def __mul__(self, otro):
if isinstance(otro, (int, float)):
return Vector2D(self.x * otro, self.y * otro)
return NotImplemented
v = Vector2D(3, 4)
print(v * 2)
for orden in ['2 * v', 'sum([v, v, v])']:
try:
print(orden, '->', eval(orden))
except TypeError as err:
print(orden, '-> TypeError:', err)
class Vector2DBien(Vector2D): # hereda todo y añade los reflejados
def __rmul__(self, otro): # otro * self (el vector está a la derecha)
return self * otro # el producto por un número es conmutativo
def __radd__(self, otro): # para sum(): empieza sumando 0 + primero
if otro == 0:
return self
return NotImplemented
v = Vector2DBien(3, 4)
print('2 * v ->', 2 * v)
print('sum ->', sum([v, Vector2DBien(1, 2), Vector2DBien(10, 10)]))
Salida:
Vector2D(6, 8) 2 * v -> TypeError: unsupported operand type(s) for *: 'int' and 'Vector2D' sum([v, v, v]) -> TypeError: unsupported operand type(s) for +: 'int' and 'Vector2D' 2 * v -> Vector2D(6, 8) sum -> Vector2D(14, 16)
Cuando el de la izquierda devuelve NotImplemented, Python no se rinde todavía: busca en el de la derecha el método reflejado, que se llama igual con una r delante (__rmul__, __radd__, __rsub__, __rtruediv__…). En 2 * v, Python prueba (2).__mul__(v), recibe NotImplemented y llama a v.__rmul__(2). El dibujo sigue los tres casos:
Vector2DBien (que hereda de Vector2D, como vimos en el tema anterior) añade dos reflejados:
__rmul__devuelveself * otro: la multiplicación por un número es conmutativa, así que basta con reutilizar__mul__.__radd__solo acepta el 0, que es lo que necesitasum(): 0 + v1 = v1, y desde ahísumsigue conv1 + v2, que ya es una suma normal de vectores.
Los que no conmutan: __rsub__ y __rtruediv__
Con la resta y la división cuidado: 10 - x no es x - 10. En __rsub__, self es el que está a la derecha de la resta:
class Euros:
def __init__(self, cantidad):
self.cantidad = cantidad
def __repr__(self):
return f'{self.cantidad} €'
def __sub__(self, otro): # euros - número
if isinstance(otro, (int, float)):
return Euros(self.cantidad - otro)
return NotImplemented
class EurosMal(Euros):
def __rsub__(self, otro): # otro - self… calculado al revés
return self - otro
class EurosBien(Euros):
def __rsub__(self, otro): # otro - self, en ese orden
return Euros(otro - self.cantidad)
print('Euros(3) - 10 =', Euros(3) - 10)
print('10 - EurosMal(3) =', 10 - EurosMal(3))
print('10 - EurosBien(3) =', 10 - EurosBien(3))
Salida:
Euros(3) - 10 = -7 € 10 - EurosMal(3) = -7 € 10 - EurosBien(3) = 7 €
EurosMal copia la idea del __rmul__ (return self - otro) y calcula 3 − 10 = −7 €, cuando 10 - EurosMal(3) debería dar 7 €. EurosBien hace la cuenta en el orden correcto: otro - self.cantidad.
Escribir __rsub__ o __rtruediv__ como return self - otro o return self / otro, igual que los conmutativos. El resultado sale con el signo cambiado (o invertido, en la división). En la práctica lo verás con fracciones: 1 - 3/4 debe dar 1/4, no -1/4.
| Expresión | Primer intento | Si devuelve NotImplemented |
|---|---|---|
v + w | v.__add__(w) | w.__radd__(v) |
2 * v | (2).__mul__(v) | v.__rmul__(2) |
10 - e | (10).__sub__(e) | e.__rsub__(10) → 10 - e, en ese orden |
sum([v, w]) | 0 + v → (0).__add__(v) | v.__radd__(0) |
—
+= y compañía: __iadd__
¿Qué hace v += w? Depende de si la clase tiene __iadd__:
- Sin
__iadd__, Python lo traduce av = v + w: llama a__add__, crea un objeto nuevo y cambia a dónde apuntav. - Con
__iadd__, llama av.__iadd__(w), que modifica el propio objeto y tiene que devolverlo (return self).
La diferencia solo se nota cuando hay otro nombre apuntando al mismo objeto:
class Vector2D: # inmutable: solo __add__
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f'Vector2D({self.x}, {self.y})'
def __add__(self, otro):
return Vector2D(self.x + otro.x, self.y + otro.y)
class Posicion(Vector2D): # mutable: añade __iadd__
def __iadd__(self, otro):
self.x += otro.x # cambia ESTE objeto…
self.y += otro.y
return self # …y lo devuelve: obligatorio
for Clase in [Vector2D, Posicion]:
v = Clase(1, 2)
alias = v # otro nombre para el mismo objeto
v += Vector2D(10, 10)
print(f'{Clase.__name__:<9} v = {v} alias = {alias} mismo objeto: {v is alias}')
Salida:
Vector2D v = Vector2D(11, 12) alias = Vector2D(1, 2) mismo objeto: False Posicion v = Vector2D(11, 12) alias = Vector2D(11, 12) mismo objeto: True
Con Vector2D, alias sigue valiendo (1, 2): v pasó a apuntar a un vector nuevo. Con Posicion, los dos nombres ven el (11, 12), porque es el mismo objeto modificado. Es lo mismo que pasa con los tipos de Python: con una tupla, t += (1,) crea una tupla nueva; con una lista, l += [1] amplía la misma lista y lo ven todos sus alias.
(¿Por qué la Posicion se imprime como Vector2D(11, 12)? Porque __repr__ escribe 'Vector2D' a mano en la madre. En el tema de herencia viste el arreglo: type(self).__name__.)
Escribir __iadd__ y olvidar el return self. Python asigna a v lo que devuelve el método, así que v pasa a valer None y el error sale en la línea siguiente ('NoneType' object has no attribute …).
—
La clase Vector2D completa
Juntando las piezas, esto es lo que suele llevar una clase «numérica» como Vector2D, y cuándo merece la pena cada cosa:
| Método | Para qué | ¿Imprescindible? |
|---|---|---|
__repr__ (y __str__) | Imprimir y depurar | Sí, siempre |
__eq__ | ==, !=, in | Si tiene sentido comparar por valor |
__hash__ | Conjuntos y claves de diccionario | Solo si los objetos no cambian |
__lt__ + @total_ordering | <, <=, >, >=, sorted, max | Si hay un orden natural |
__add__, __sub__, __mul__, __truediv__ | Aritmética | Si las operaciones tienen sentido |
__radd__, __rmul__, __rsub__, __rtruediv__ | Número a la izquierda, sum() | Si se mezcla con números |
__neg__, __abs__, __bool__ | -v, abs(v), if v: | Según el caso |
__iadd__ | += modificando el objeto | Solo en clases mutables |
La regla de oro de la sobrecarga: que el operador haga lo que cualquiera esperaría. Un + que borra cosas o un < que compara algo extraño compila igual, pero confunde a quien lea tu código. Si la operación no es evidente, mejor un método con nombre (v.rotar(90)).
—
Sobrecarga de funciones: polimorfismo con parámetros
En Java (y en C++) se puede escribir varias veces un método con el mismo nombre y distintos parámetros: es la sobrecarga de métodos. En Python no existe: un segundo def con el mismo nombre sustituye al primero.
def area(lado):
return lado * lado
def area(base, altura): # NO sobrecarga: sustituye a la de arriba
return base * altura
print(area(3, 4))
try:
print(area(3))
except TypeError as err:
print('TypeError:', err)
def area_bien(base, altura=None): # un parámetro opcional hace las dos cosas
if altura is None:
return base * base # cuadrado
return base * altura # rectángulo
print(area_bien(3), area_bien(3, 4))
Salida:
12 TypeError: area() missing 1 required positional argument: 'altura' 9 12
area(3, 4) funciona porque la segunda definición pisó a la primera, y area(3) da TypeError: la de un parámetro ya no existe. En Python se consigue lo mismo con una sola función que admite varias formas de llamarla: parámetros con valor por defecto (altura=None), *args o comprobar el tipo con isinstance dentro. Y en las clases, el polimorfismo que viste en herencia: el mismo método en clases distintas.
«Sobrecarga de operadores» y «sobrecarga de métodos» suenan parecido, pero son cosas distintas. La primera (dar significado a + en tu clase) existe en Python; la segunda (varios métodos con el mismo nombre y distinta firma), no.
—
Errores típicos con la sobrecarga de operadores
__eq__ sin isinstance: v == 5 lanza AttributeError en lugar de dar False.
Definir __eq__ y querer meter los objetos en un set: TypeError: unhashable type. Falta __hash__.
Devolver False, None o lanzar TypeError cuando el tipo no encaja, en vez de return NotImplemented.
Olvidar los reflejados: v * 2 funciona y 2 * v no; sum() de tus objetos falla con 'int' and 'Vector2D'.
__rsub__ escrito como self - otro: el resultado sale con el signo cambiado.
__iadd__ sin return self: la variable pasa a valer None.
Escribir solo __str__: las listas siguen saliendo con <… object at 0x…>.
—
Míralo paso a paso en Python Tutor
pythontutor.com dibuja los objetos y la pila de llamadas, y ahí se ve que un operador es una llamada a un método. Elige Python 3 y pega:
class Vector2D:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, otro):
return Vector2D(self.x + otro.x, self.y + otro.y)
def __mul__(self, n):
return Vector2D(self.x * n, self.y * n)
def __rmul__(self, n):
return self * n
v = Vector2D(3, 4)
w = v + Vector2D(1, 2)
z = 2 * v
print(w.x, w.y, z.x, z.y)
Salida:
4 6 6 8
Qué observar paso a paso:
- En la línea
w = v + Vector2D(1, 2)verás aparecer un marco de__add__en la pila, conselfapuntando avyotroal vector nuevo. El+se convirtió en una llamada. - Dentro de
__add__se crea un tercer objeto, el resultado. Fíjate en quevno cambia. - En
z = 2 * v, Python no entra en ningún método del 2 (los enteros no salen en la pila): salta directamente a__rmul__dev, connvaliendo 2. - Desde
__rmul__aparece un segundo marco, el de__mul__, por elreturn self * n. Dos marcos apilados para un solo*.
—
Resumen rápido
| Quiero… | Escribo | Detalle |
|---|---|---|
| Imprimir bonito | __str__ y __repr__ | Las listas usan __repr__ |
== por valor | __eq__ + isinstance | return NotImplemented si el tipo no encaja |
Usar el objeto en un set | __hash__ | Iguales → mismo hash |
| Ordenar | __eq__ + __lt__ + @total_ordering | El decorador genera <=, >, >= |
+, -, *, / | __add__, __sub__, __mul__, __truediv__ | Devuelven un objeto nuevo |
| Número a la izquierda | __radd__, __rmul__, __rsub__, __rtruediv__ | Resta y división no conmutan |
+= que modifica | __iadd__ | Siempre return self |
| Varias «versiones» de una función | Parámetros por defecto | En Python no hay sobrecarga de funciones |
Tres ideas para quedarte:
- Un operador es una llamada a un método:
a + besa.__add__(b). Sobrecargar es escribir ese método. NotImplementedno es un error, es «pregunta al otro»: así funcionan los reflejados y los mensajes de error claros.- En los reflejados,
selfestá a la derecha: la suma y el producto conmutan; la resta y la división, no.
En la práctica de sobrecarga de operadores construimos tres clases completas: fracciones que se simplifican solas, dinero sin errores de céntimos (y con conversión de moneda) y una estantería de la biblioteca que se usa como una lista.
Siguiente paso: prácticaSobrecarga de operadores en Python práctica — fracciones que se simplifican solas, dinero sin errores de céntimos y la estantería de la biblioteca (y el bote de la cena que no cuadraba por un céntimo)¿Te ha servido este artículo? Así puedes apoyar SergioLearns →




El hardware que sostiene al sistema operativo — teoría con la práctica de IC2 en la impresora, una interrupción que lo para todo y una caché que acierta (y el bucle infinito que no colgó mi portátil)