Sobrecarga de operadores en Python — ejercicios para dominar los métodos mágicos
La sobrecarga de operadores en Python ejercicios con solución cierran este bloque. Ya viste la teoría y practicaste con clases reales. Ahora toca resolver por tu cuenta. Dos ejercicios en tres niveles con especial atención al error más traicionero, implementar __rsub__ o __rtruediv__ como si la operación fuera conmutativa cuando no lo es. Recuerda revisar tus erroes con pythontutor.com.
Tabla de Contenidos
Sobrecarga de operadores en Python ejercicios — Nivel Básico
Ejercicio 1 — Clase Número Complejo
Implementa una clase Complejo que represente números complejos de la forma a + bi. Un número complejo tiene parte real a y parte imaginaria b.
Requisitos:
- Atributos privados
realeimaginariocon@property - Operadores de comparación — dos complejos son iguales si tienen la misma parte real e imaginaria. No tiene sentido ordenarlos con
<o>— no los implementes - Operadores aritméticos:
+,-,*entre complejos y entre complejo y número real - Operadores reflejados para que
2 + cfuncione igual quec + 2 - Operador unario
-y funciónabs()— el módulo del complejo es√(a² + b²) - Método
conjugado()— devuelve el complejo con la parte imaginaria negada __str__con formatoa+bioa-bisegún el signo__repr__con formatoComplejo(a, b)
La salida debe ser:
c1 = 3+4i c2 = 1-2i repr: Complejo(3, 4) --- Aritmética --- c1 + c2 = 4+2i c1 - c2 = 2+6i c1 * c2 = 11-2i c1 + 2 = 5+4i 2 + c1 = 5+4i 3 - c1 = 0-4i ← __rsub__ — no conmutativo 2 * c1 = 6+8i --- Unarios y módulo --- -c1 = -3-4i abs(c1) = 5.0 c1.conjugado() = 3-4i --- Comparación --- c1 == Complejo(3, 4): True c1 == c2: False
💡 Pista — el error de los operadores reflejados no conmutativos:
# MAL — tratas la resta como si fuera conmutativa
def __rsub__(self, other):
return self.__sub__(other) # calcula self - other
# pero debería ser other - self
# Comprobación:
c = Complejo(3, 4)
print(3 - c) # debería ser 0-4i (3-3 + 0-4i)
# con el error da -0-4i (3-3 + 0-(-4)i) → MAL
# BIEN — inviertes el orden
def __rsub__(self, other):
if isinstance(other, (int, float)):
return Complejo(other - self.real, -self.imaginario)
return NotImplemented
La resta 3 - c significa 3 - (3+4i) = (3-3) + (0-4)i = 0-4i. Si lo calculas como c - 3 obtienes (3-3) + 4i = 0+4i — resultado opuesto en la parte imaginaria.
Sobrecarga de operadores en Python ejercicios — Nivel Intermedio y Desafío
Ejercicio 2 — Clase Polinomio
Implementa una clase Polinomio que represente un polinomio de una variable. Un polinomio se representa por sus coeficientes, la lista [2, -3, 1] representa 2x² - 3x + 1.
Requisitos:
Básico:
- Atributo privado
coeficientes— lista donde el índice es el grado del término grado()— devuelve el grado del polinomio (índice del coeficiente más alto no nulo)evaluar(x)— evalúa el polinomio en un punto__str__con formato legible:2x² - 3x + 1__repr__:Polinomio([2, -3, 1])
Intermedio:
__add__y__sub__entre polinomios y entre polinomio y número__radd__y__rsub__— recuerda que la resta no es conmutativa__mul__entre polinomios y entre polinomio y número (escalar)__rmul__para2 * p
Desafío:
__eq__— dos polinomios son iguales si tienen los mismos coeficientes (ignorando ceros al final)__neg__— niega todos los coeficientes__call__— permite usar el polinomio como función:p(3)equivale ap.evaluar(3)derivada()— devuelve el polinomio derivada
La salida debe ser:
p1 = 2x² - 3x + 1 p2 = x + 2 repr: Polinomio([1, -3, 2]) --- Evaluación --- p1(0) = 1 p1(1) = 0 p1(3) = 10 p2(2) = 4 --- Aritmética --- p1 + p2 = 2x² - 2x + 3 p1 - p2 = 2x² - 4x - 1 p1 * p2 = 2x³ + x² - 5x + 2 p1 * 2 = 4x² - 6x + 2 2 * p1 = 4x² - 6x + 2 3 - p1 = -2x² + 3x + 2 ← __rsub__ — no conmutativo --- Derivada --- p1' = 4x - 3 p2' = 1 --- Comparación --- p1 == Polinomio([1, -3, 2]): True
💡 Pistas:
- Guarda los coeficientes en orden ascendente de grado —
[1, -3, 2]=1 + (-3)x + 2x² - Para
__add__rellena con ceros el polinomio más corto antes de sumar - Para
__mul__entre polinomios usa la propiedad distributiva — cada término del primero multiplica a todos del segundo - El error de
__rsub__:3 - pno esp - 3— esPolinomio([3]) - p - Para
__str__trata por separado el término independiente, los lineales y los de grado > 1 - La derivada de
aₙxⁿesn·aₙxⁿ⁻¹
Soluciones Comentadas
Solución Ejercicio 1:
import math
from functools import total_ordering
class Complejo:
def __init__(self, real, imaginario=0):
if not isinstance(real, (int, float)):
raise TypeError('La parte real debe ser numérica')
if not isinstance(imaginario, (int, float)):
raise TypeError('La parte imaginaria debe ser numérica')
self.__real = float(real)
self.__imaginario = float(imaginario)
@property
def real(self):
return self.__real
@property
def imaginario(self):
return self.__imaginario
def conjugado(self):
return Complejo(self.__real, -self.__imaginario)
def modulo(self):
return math.sqrt(self.__real**2 + self.__imaginario**2)
# Comparación — solo igualdad, sin orden
def __eq__(self, other):
if isinstance(other, Complejo):
return (abs(self.__real - other.real) < 1e-9 and
abs(self.__imaginario - other.imaginario) < 1e-9)
elif isinstance(other, (int, float)):
return self.__real == other and self.__imaginario == 0
return False
# Aritmética
def __add__(self, other):
if isinstance(other, Complejo):
return Complejo(self.__real + other.real,
self.__imaginario + other.imaginario)
elif isinstance(other, (int, float)):
return Complejo(self.__real + other, self.__imaginario)
return NotImplemented
def __radd__(self, other):
return self.__add__(other) # suma ES conmutativa
def __sub__(self, other):
if isinstance(other, Complejo):
return Complejo(self.__real - other.real,
self.__imaginario - other.imaginario)
elif isinstance(other, (int, float)):
return Complejo(self.__real - other, self.__imaginario)
return NotImplemented
def __rsub__(self, other):
# other - self ≠ self - other
if isinstance(other, (int, float)):
return Complejo(other - self.__real, -self.__imaginario)
return NotImplemented
def __mul__(self, other):
if isinstance(other, Complejo):
# (a+bi)(c+di) = (ac-bd) + (ad+bc)i
real = self.__real * other.real - self.__imaginario * other.imaginario
imag = self.__real * other.imaginario + self.__imaginario * other.real
return Complejo(real, imag)
elif isinstance(other, (int, float)):
return Complejo(self.__real * other, self.__imaginario * other)
return NotImplemented
def __rmul__(self, other):
return self.__mul__(other) # multiplicación ES conmutativa
def __neg__(self):
return Complejo(-self.__real, -self.__imaginario)
def __abs__(self):
return self.modulo()
def __str__(self):
if self.__imaginario == 0:
return str(self.__real)
signo = '+' if self.__imaginario >= 0 else ''
real_str = f'{self.__real:.6g}'
imag_str = f'{self.__imaginario:.6g}'
return f'{real_str}{signo}{imag_str}i'
def __repr__(self):
return f'Complejo({self.__real}, {self.__imaginario})'
# Uso
c1 = Complejo(3, 4)
c2 = Complejo(1, -2)
print(f'c1 = {c1}')
print(f'c2 = {c2}')
print(f'repr: {repr(c1)}\n')
print('--- Aritmética ---')
print(f'c1 + c2 = {c1 + c2}')
print(f'c1 - c2 = {c1 - c2}')
print(f'c1 * c2 = {c1 * c2}')
print(f'c1 + 2 = {c1 + 2}')
print(f'2 + c1 = {2 + c1}')
print(f'3 - c1 = {3 - c1}')
print(f'2 * c1 = {2 * c1}\n')
print('--- Unarios y módulo ---')
print(f'-c1 = {-c1}')
print(f'abs(c1) = {abs(c1)}')
print(f'c1.conjugado() = {c1.conjugado()}\n')
print('--- Comparación ---')
print(f'c1 == Complejo(3, 4): {c1 == Complejo(3, 4)}')
print(f'c1 == c2: {c1 == c2}')
Solución Ejercicio 2:
class Polinomio:
def __init__(self, coeficientes):
if not coeficientes:
raise ValueError('El polinomio debe tener al menos un coeficiente')
# Eliminar ceros al final
self.__coef = list(coeficientes)
while len(self.__coef) > 1 and self.__coef[-1] == 0:
self.__coef.pop()
@property
def coeficientes(self):
return list(self.__coef)
def grado(self):
return len(self.__coef) - 1
def evaluar(self, x):
resultado = 0
for i, c in enumerate(self.__coef):
resultado += c * (x ** i)
return resultado
def __call__(self, x):
return self.evaluar(x)
def derivada(self):
if self.grado() == 0:
return Polinomio([0])
nuevos = [i * c for i, c in enumerate(self.__coef) if i > 0]
return Polinomio(nuevos)
# Comparación
def __eq__(self, other):
if isinstance(other, Polinomio):
return self.__coef == other.coeficientes
return False
# Aritmética
def __add__(self, other):
if isinstance(other, Polinomio):
n = max(len(self.__coef), len(other.coeficientes))
c1 = self.__coef + [0] * (n - len(self.__coef))
c2 = other.coeficientes + [0] * (n - len(other.coeficientes))
return Polinomio([a + b for a, b in zip(c1, c2)])
elif isinstance(other, (int, float)):
nuevos = list(self.__coef)
nuevos[0] += other
return Polinomio(nuevos)
return NotImplemented
def __radd__(self, other):
return self.__add__(other) # suma ES conmutativa
def __sub__(self, other):
if isinstance(other, Polinomio):
return self + (-other)
elif isinstance(other, (int, float)):
nuevos = list(self.__coef)
nuevos[0] -= other
return Polinomio(nuevos)
return NotImplemented
def __rsub__(self, other):
# other - self ≠ self - other
if isinstance(other, (int, float)):
return Polinomio([other]) - self
return NotImplemented
def __mul__(self, other):
if isinstance(other, Polinomio):
n = len(self.__coef) + len(other.coeficientes) - 1
resultado = [0] * n
for i, a in enumerate(self.__coef):
for j, b in enumerate(other.coeficientes):
resultado[i + j] += a * b
return Polinomio(resultado)
elif isinstance(other, (int, float)):
return Polinomio()
return NotImplemented
def __rmul__(self, other):
return self.__mul__(other) # multiplicación escalar ES conmutativa
def __neg__(self):
return Polinomio([-c for c in self.__coef])
def __str__(self):
if all(c == 0 for c in self.__coef):
return '0'
terminos = []
for i in range(len(self.__coef) - 1, -1, -1):
c = self.__coef[i]
if c == 0:
continue
if i == 0:
terminos.append(str(int(c) if c == int(c) else c))
elif i == 1:
if c == 1: terminos.append('x')
elif c == -1: terminos.append('-x')
else: terminos.append(f'{int(c) if c == int(c) else c}x')
else:
if c == 1: terminos.append(f'x{chr(8304+i) if i < 10 else f"^{i}"}')
elif c == -1: terminos.append(f'-x{chr(8304+i) if i < 10 else f"^{i}"}')
else:
exp = chr(8304 + i) if i < 10 else f'^{i}'
terminos.append(f'{int(c) if c == int(c) else c}x{exp}')
resultado = terminos[0]
for t in terminos[1:]:
if t.startswith('-'):
resultado += f' - {t[1:]}'
else:
resultado += f' + {t}'
return resultado
def __repr__(self):
return f'Polinomio({self.__coef})'
# Uso
p1 = Polinomio([1, -3, 2]) # 2x² - 3x + 1
p2 = Polinomio([2, 1]) # x + 2
print(f'p1 = {p1}')
print(f'p2 = {p2}')
print(f'repr: {repr(p1)}\n')
print('--- Evaluación ---')
print(f'p1(0) = {p1(0)}')
print(f'p1(1) = {p1(1)}')
print(f'p1(3) = {p1(3)}')
print(f'p2(2) = {p2(2)}\n')
print('--- Aritmética ---')
print(f'p1 + p2 = {p1 + p2}')
print(f'p1 - p2 = {p1 - p2}')
print(f'p1 * p2 = {p1 * p2}')
print(f'p1 * 2 = {p1 * 2}')
print(f'2 * p1 = {2 * p1}')
print(f'3 - p1 = {3 - p1}\n')
print('--- Derivada ---')
print(f"p1' = {p1.derivada()}")
print(f"p2' = {p2.derivada()}\n")
print('--- Comparación ---')
print(f'p1 == Polinomio([1, -3, 2]): {p1 == Polinomio([1, -3, 2])}')
Chuletario — Sobrecarga de operadores en Python
# ============================================
# CHULETARIO — Sobrecarga de operadores
# Sergio Learns · sergiolearns.com
# ============================================
# COMPARACIÓN CON @total_ordering
from functools import total_ordering
@total_ordering
class MiClase:
def __eq__(self, other):
if not isinstance(other, MiClase): return False
return ... # comparación real
def __lt__(self, other):
if not isinstance(other, MiClase): return NotImplemented
return ... # @total_ordering genera >, <=, >=
# TODOS LOS OPERADORES DE COMPARACIÓN
# __eq__ → == __ne__ → != (auto si defines __eq__)
# __lt__ → < __gt__ → >
# __le__ → <= __ge__ → >=
# ARITMÉTICA BINARIA — self OP other
# __add__ → + __radd__ → + (reflejado)
# __sub__ → - __rsub__ → - (reflejado)
# __mul__ → * __rmul__ → * (reflejado)
# __truediv__ → / __rtruediv__ → / (reflejado)
# __floordiv__ → // __mod__ → %
# __pow__ → **
# PATRÓN ARITMÉTICO CORRECTO
def __add__(self, other):
if isinstance(other, MiClase):
return MiClase(...)
elif isinstance(other, (int, float)):
return MiClase(...)
return NotImplemented # NUNCA None
# OPERADORES REFLEJADOS — cuando self está a la DERECHA
# Python prueba: type(a).__add__(a, b)
# Si NotImplemented → type(b).__radd__(b, a)
# Si NotImplemented → TypeError
# CONMUTATIVOS — reflejado igual al directo
def __radd__(self, other): return self.__add__(other)
def __rmul__(self, other): return self.__mul__(other)
# NO CONMUTATIVOS — reflejado invertido
def __rsub__(self, other):
# other - self ≠ self - other
if isinstance(other, (int, float)):
return MiClase(other - self.valor) # orden invertido
return NotImplemented
def __rtruediv__(self, other):
# other / self ≠ self / other
if isinstance(other, (int, float)):
return MiClase(other / self.valor) # orden invertido
return NotImplemented
# UNARIOS
# __neg__ → -obj
# __pos__ → +obj
# __abs__ → abs(obj)
# __round__ → round(obj, n)
# __invert__ → ~obj
def __neg__(self): return MiClase(-self.valor)
def __abs__(self): return MiClase(abs(self.valor))
def __round__(self, n=0): return MiClase(round(self.valor, n))
# CONVERSIÓN
# __int__ → int(obj)
# __float__ → float(obj)
# __bool__ → bool(obj)
# __str__ → str(obj) / print(obj)
# __repr__ → repr(obj)
# __len__ → len(obj)
# __call__ → obj(args) — hace el objeto llamable como función
def __call__(self, x):
return self.evaluar(x) # obj(x) equivale a obj.evaluar(x)
# CONTENEDORES
# __len__ → len(obj)
# __getitem__ → obj[key]
# __setitem__ → obj[key] = value
# __contains__ → x in obj
# __iter__ → for x in obj
# POLIMORFISMO SIN SOBRECARGA REAL
# Parámetros por omisión
def __init__(self, param1, param2=None):
if param2 is None:
... # comportamiento con un parámetro
else:
... # comportamiento con dos parámetros
# Por tipo de parámetro
def metodo(self, other):
if isinstance(other, MiClase): ...
elif isinstance(other, int): ...
elif isinstance(other, float): ...
else: return NotImplemented
# ERRORES TÍPICOS
# 1. return None en vez de NotImplemented → rompe los reflejados
# 2. __rsub__ igual que __sub__ → resultado incorrecto
# 3. __rtruediv__ igual que __truediv__ → resultado incorrecto
# 4. No comprobar isinstance → AttributeError al acceder a atributos
# 5. @total_ordering sin __eq__ → no funciona
# 6. Comparar con == sin isinstance → puede comparar referencias
# FLUJO COMPLETO Python con a OP b
# 1. type(a).__op__(a, b) → si NotImplemented:
# 2. type(b).__rop__(b, a) → si NotImplemented:
# 3. TypeError: unsupported operand type(s)
# REGLA PRÁCTICA
# + * → conmutativos → __radd__ = __add__, __rmul__ = __mul__
# - / → NO conmutativos → __rsub__ y __rtruediv__ invertidos
Python operator overloading — exercises to master magic methods
Basic Level
Exercise 1 — Complex Number class
Implement a Complex class representing complex numbers a + bi. Private attributes real and imaginary with @property. Equality only, no ordering. Arithmetic: +, -, * between complex numbers and with real numbers. Reflected operators. Unary - and abs(). conjugate() method. __str__ as a+bi or a-bi. __repr__ as Complex(a, b).
💡 Hint — non-commutative reflected operators:
# WRONG — treats subtraction as commutative
def __rsub__(self, other):
return self.__sub__(other) # calculates self - other
# should be other - self
# Check:
c = Complex(3, 4)
print(3 - c) # should be 0-4i
# with mistake gives 0+4i — wrong sign on imaginary
# RIGHT — invert the order
def __rsub__(self, other):
if isinstance(other, (int, float)):
return Complex(other - self.real, -self.imaginary)
return NotImplemented
3 - c means 3 - (3+4i) = (3-3) + (0-4)i = 0-4i. Calculating as c - 3 gives 0+4i — opposite imaginary sign.
Intermediate and Challenge Level
Exercise 2 — Polynomial class
Coefficients stored in ascending degree order, [1, -3, 2] = 1 - 3x + 2x².
Basic: degree(), evaluate(x), __str__, __repr__. Intermediate: __add__, __sub__, __mul__ between polynomials and with scalars. Reflected operators. Challenge: __eq__, __neg__, __call__ (makes p(3) work like p.evaluate(3)), derivative().
💡 Hints: Pad the shorter polynomial with zeros before adding. For __mul__ use distributive property. The __rsub__ mistake: 3 - p is not p - 3 — it’s Polynomial([3]) - p. Derivative of aₙxⁿ is n·aₙxⁿ⁻¹.
(Solutions same as Spanish version above)
Cheat sheet — Python Operator Overloading
# ============================================
# CHEAT SHEET — Operator Overloading
# Sergio Learns · sergiolearns.com
# ============================================
# COMPARISON WITH @total_ordering
from functools import total_ordering
@total_ordering
class MyClass:
def __eq__(self, other):
if not isinstance(other, MyClass): return False
return ...
def __lt__(self, other):
if not isinstance(other, MyClass): return NotImplemented
return ... # @total_ordering generates >, <=, >=
# ALL COMPARISON OPERATORS
# __eq__ → == __ne__ → != (auto if __eq__ defined)
# __lt__ → < __gt__ → >
# __le__ → <= __ge__ → >=
# ARITHMETIC — self OP other
# __add__ → + __radd__ → + (reflected)
# __sub__ → - __rsub__ → - (reflected)
# __mul__ → * __rmul__ → * (reflected)
# __truediv__ → / __rtruediv__ → / (reflected)
# CORRECT ARITHMETIC PATTERN
def __add__(self, other):
if isinstance(other, MyClass): return MyClass(...)
elif isinstance(other, (int, float)): return MyClass(...)
return NotImplemented # NEVER None
# REFLECTED OPERATORS — when self is on the RIGHT
# Python tries: type(a).__add__(a, b)
# If NotImplemented → type(b).__radd__(b, a)
# If NotImplemented → TypeError
# COMMUTATIVE — reflected same as direct
def __radd__(self, other): return self.__add__(other)
def __rmul__(self, other): return self.__mul__(other)
# NOT COMMUTATIVE — reflected inverted
def __rsub__(self, other):
# other - self ≠ self - other
if isinstance(other, (int, float)):
return MyClass(other - self.value) # inverted order
return NotImplemented
def __rtruediv__(self, other):
# other / self ≠ self / other
if isinstance(other, (int, float)):
return MyClass(other / self.value) # inverted order
return NotImplemented
# UNARY
def __neg__(self): return MyClass(-self.value)
def __abs__(self): return MyClass(abs(self.value))
def __round__(self, n=0): return MyClass(round(self.value, n))
# CONVERSION
# __int__ → int(obj) __float__ → float(obj)
# __bool__ → bool(obj) __call__ → obj(args)
# POLYMORPHISM WITHOUT REAL OVERLOADING
def method(self, other):
if isinstance(other, MyClass): ...
elif isinstance(other, int): ...
else: return NotImplemented
# COMMON MISTAKES
# 1. return None instead of NotImplemented → breaks reflected ops
# 2. __rsub__ same as __sub__ → wrong result
# 3. __rtruediv__ same as __truediv__ → wrong result
# 4. No isinstance check → AttributeError
# 5. @total_ordering without __eq__ → doesn't work
# PRACTICAL RULE
# + * → commutative → __radd__ = __add__, __rmul__ = __mul__
# - / → NOT commutative → __rsub__ and __rtruediv__ inverted
# Python operator resolution for a OP b:
# 1. type(a).__op__(a, b) → if NotImplemented:
# 2. type(b).__rop__(b, a) → if NotImplemented:
# 3. TypeError
