Sobrecarga de operadores en Python — 2 clases reales con todos los operadores
La sobrecarga de operadores en Python práctica real es lo que toca ahora. En el artículo anterior vimos la teoría. Ahora construimos dos clases completas con todos los operadores, número racional y dinero con conversión de moneda. Recuerda que usando pythontutor.com, puedes también visualizar tu código.
Tabla de Contenidos
Sobrecarga de operadores en Python práctica — Programa 1: Clase Número Racional
El número racional es el ejemplo clásico de FP2 para sobrecarga de operadores, tiene numerador y denominador, no puede tener denominador cero y opera con las reglas del álgebra de fracciones:
from functools import total_ordering
from math import gcd
@total_ordering
class Racional:
def __init__(self, numerador, denominador=1):
if not isinstance(numerador, int):
raise TypeError('El numerador debe ser un entero')
if not isinstance(denominador, int):
raise TypeError('El denominador debe ser un entero')
if denominador == 0:
raise ZeroDivisionError('El denominador no puede ser cero')
# Normalizar signo — el signo siempre en el numerador
if denominador < 0:
numerador = -numerador
denominador = -denominador
# Simplificar usando el máximo común divisor
comun = gcd(abs(numerador), denominador)
self.__num = numerador // comun
self.__den = denominador // comun
@property
def numerador(self):
return self.__num
@property
def denominador(self):
return self.__den
# Métodos de clase alternativos para crear racionales
@classmethod
def desde_float(cls, valor, precision=6):
"""Crea un racional aproximado desde un float"""
denominador = 10 ** precision
numerador = round(valor * denominador)
return cls(numerador, denominador)
# Comparación
def __eq__(self, other):
if isinstance(other, Racional):
return self.__num == other.numerador and self.__den == other.denominador
elif isinstance(other, int):
return self.__num == other and self.__den == 1
return False
def __lt__(self, other):
if isinstance(other, Racional):
return self.__num * other.denominador < other.numerador * self.__den
elif isinstance(other, int):
return self.__num < other * self.__den
return NotImplemented
# Aritmética
def __add__(self, other):
if isinstance(other, Racional):
nuevo_num = self.__num * other.denominador + other.numerador * self.__den
nuevo_den = self.__den * other.denominador
return Racional(nuevo_num, nuevo_den)
elif isinstance(other, int):
return self + Racional(other)
return NotImplemented
def __radd__(self, other):
return self.__add__(other)
def __sub__(self, other):
if isinstance(other, Racional):
nuevo_num = self.__num * other.denominador - other.numerador * self.__den
nuevo_den = self.__den * other.denominador
return Racional(nuevo_num, nuevo_den)
elif isinstance(other, int):
return self - Racional(other)
return NotImplemented
def __rsub__(self, other):
if isinstance(other, int):
return Racional(other) - self
return NotImplemented
def __mul__(self, other):
if isinstance(other, Racional):
return Racional(self.__num * other.numerador,
self.__den * other.denominador)
elif isinstance(other, int):
return Racional(self.__num * other, self.__den)
return NotImplemented
def __rmul__(self, other):
return self.__mul__(other)
def __truediv__(self, other):
if isinstance(other, Racional):
if other.numerador == 0:
raise ZeroDivisionError('No se puede dividir por cero')
return Racional(self.__num * other.denominador,
self.__den * other.numerador)
elif isinstance(other, int):
if other == 0:
raise ZeroDivisionError('No se puede dividir por cero')
return Racional(self.__num, self.__den * other)
return NotImplemented
def __rtruediv__(self, other):
if isinstance(other, int):
return Racional(other) / self
return NotImplemented
# Unarios
def __neg__(self):
return Racional(-self.__num, self.__den)
def __pos__(self):
return Racional(self.__num, self.__den)
def __abs__(self):
return Racional(abs(self.__num), self.__den)
# Conversión
def __float__(self):
return self.__num / self.__den
def __int__(self):
return self.__num // self.__den
def __round__(self, n=0):
return round(float(self), n)
# Representación
def __str__(self):
if self.__den == 1:
return str(self.__num)
return f'{self.__num}/{self.__den}'
def __repr__(self):
return f'Racional({self.__num}, {self.__den})'
# Uso completo
r1 = Racional(3, 4)
r2 = Racional(1, 2)
r3 = Racional(6, 8) # se simplifica a 3/4
print(f'r1 = {r1}') # → 3/4
print(f'r2 = {r2}') # → 1/2
print(f'r3 = {r3}') # → 3/4 (simplificado)
print(f'\n--- Aritmética ---')
print(f'r1 + r2 = {r1 + r2}') # → 5/4
print(f'r1 - r2 = {r1 - r2}') # → 1/4
print(f'r1 * r2 = {r1 * r2}') # → 3/8
print(f'r1 / r2 = {r1 / r2}') # → 3/2
print(f'r1 + 1 = {r1 + 1}') # → 7/4
print(f'2 * r1 = {2 * r1}') # → 3/2 (reflejado)
print(f'1 - r1 = {1 - r1}') # → 1/4 (__rsub__)
print(f'\n--- Comparación ---')
print(f'r1 == r3: {r1 == r3}') # → True (3/4 == 3/4)
print(f'r1 > r2: {r1 > r2}') # → True
print(f'r2 < r1: {r2 < r1}') # → True
print(f'\n--- Unarios ---')
print(f'-r1 = {-r1}') # → -3/4
print(f'abs(-r1) = {abs(-r1)}') # → 3/4
print(f'\n--- Conversión ---')
print(f'float(r1) = {float(r1)}') # → 0.75
print(f'int(r1) = {int(r1)}') # → 0
print(f'round(r1, 2) = {round(r1, 2)}') # → 0.75
print(f'\n--- Desde float ---')
r4 = Racional.desde_float(0.75)
print(f'0.75 = {r4}') # → 3/4
print(f'\n--- Ordenación ---')
racionales = [Racional(3,4), Racional(1,2), Racional(1,3), Racional(2,3)]
print(sorted(racionales)) # → [1/3, 1/2, 2/3, 3/4]
r1 = 3/4 r2 = 1/2 r3 = 3/4 --- Aritmética --- r1 + r2 = 5/4 r1 - r2 = 1/4 r1 * r2 = 3/8 r1 / r2 = 3/2 r1 + 1 = 7/4 2 * r1 = 3/2 1 - r1 = 1/4 --- Comparación --- r1 == r3: True r1 > r2: True r2 < r1: True --- Unarios --- -r1 = -3/4 abs(-r1) = 3/4 --- Conversión --- float(r1) = 0.75 int(r1) = 0 round(r1, 2) = 0.75 --- Desde float --- 0.75 = 3/4 --- Ordenación --- [1/3, 1/2, 2/3, 3/4]
Tres detalles importantes de esta implementación:
Primero — en __init__ normalizamos el signo y simplificamos con gcd. Así Racional(6, 8) y Racional(3, 4) son iguales desde el momento de la creación.
Segundo — __rsub__ no es simplemente self.__sub__(other) porque la resta no es conmutativa. 1 - r1 es Racional(1) - r1, no r1 - Racional(1).
Tercero — __float__, __int__ y __round__ permiten usar float(), int() y round() directamente sobre racionales — igual que con los tipos predefinidos.
Sobrecarga de operadores en Python práctica — Programa 2: Clase Dinero con conversión de moneda
from functools import total_ordering
@total_ordering
class Dinero:
# Tasas de cambio respecto al euro (atributo de clase)
_tasas = {
'EUR': 1.0,
'USD': 1.09,
'GBP': 0.86,
'JPY': 162.50,
'CHF': 0.97
}
def __init__(self, cantidad, moneda='EUR'):
if not isinstance(cantidad, (int, float)):
raise TypeError('La cantidad debe ser numérica')
if cantidad < 0:
raise ValueError('La cantidad no puede ser negativa')
if moneda not in Dinero._tasas:
raise ValueError(f'Moneda no soportada: {moneda}. '
f'Disponibles: {list(Dinero._tasas.keys())}')
self.__cantidad = round(float(cantidad), 2)
self.__moneda = moneda
@property
def cantidad(self):
return self.__cantidad
@property
def moneda(self):
return self.__moneda
def convertir(self, moneda_destino):
"""Convierte a otra moneda usando las tasas de cambio"""
if moneda_destino not in Dinero._tasas:
raise ValueError(f'Moneda no soportada: {moneda_destino}')
# Convertir a EUR primero, luego a destino
en_euros = self.__cantidad / Dinero._tasas[self.__moneda]
en_destino = en_euros * Dinero._tasas[moneda_destino]
return Dinero(round(en_destino, 2), moneda_destino)
def _en_euros(self):
"""Valor en euros para comparaciones"""
return self.__cantidad / Dinero._tasas[self.__moneda]
@classmethod
def actualizar_tasa(cls, moneda, tasa):
"""Actualiza la tasa de cambio de una moneda"""
if tasa <= 0:
raise ValueError('La tasa debe ser positiva')
cls._tasas[moneda] = tasa
# Comparación — compara valores en euros
def __eq__(self, other):
if isinstance(other, Dinero):
return abs(self._en_euros() - other._en_euros()) < 0.001
elif isinstance(other, (int, float)):
return self._en_euros() == other
return False
def __lt__(self, other):
if isinstance(other, Dinero):
return self._en_euros() < other._en_euros()
elif isinstance(other, (int, float)):
return self._en_euros() < other
return NotImplemented
# Aritmética — opera en la moneda del operando izquierdo
def __add__(self, other):
if isinstance(other, Dinero):
otro_convertido = other.convertir(self.__moneda)
return Dinero(self.__cantidad + otro_convertido.cantidad,
self.__moneda)
elif isinstance(other, (int, float)):
return Dinero(self.__cantidad + other, self.__moneda)
return NotImplemented
def __radd__(self, other):
return self.__add__(other)
def __sub__(self, other):
if isinstance(other, Dinero):
otro_convertido = other.convertir(self.__moneda)
resultado = self.__cantidad - otro_convertido.cantidad
if resultado < 0:
raise ValueError('El resultado no puede ser negativo')
return Dinero(resultado, self.__moneda)
elif isinstance(other, (int, float)):
resultado = self.__cantidad - other
if resultado < 0:
raise ValueError('El resultado no puede ser negativo')
return Dinero(resultado, self.__moneda)
return NotImplemented
def __mul__(self, other):
if isinstance(other, (int, float)):
return Dinero(self.__cantidad * other, self.__moneda)
return NotImplemented
def __rmul__(self, other):
return self.__mul__(other)
def __truediv__(self, other):
if isinstance(other, (int, float)):
if other == 0:
raise ZeroDivisionError('No se puede dividir por cero')
return Dinero(self.__cantidad / other, self.__moneda)
elif isinstance(other, Dinero):
# División de dinero → ratio sin moneda
otro_convertido = other.convertir(self.__moneda)
return self.__cantidad / otro_convertido.cantidad
return NotImplemented
def __neg__(self):
raise ValueError('El dinero no puede ser negativo')
def __abs__(self):
return Dinero(abs(self.__cantidad), self.__moneda)
def __str__(self):
simbolos = {'EUR': '€', 'USD': '$', 'GBP': '£',
'JPY': '¥', 'CHF': 'Fr'}
simbolo = simbolos.get(self.__moneda, self.__moneda)
return f'{self.__cantidad:.2f}{simbolo}'
def __repr__(self):
return f'Dinero({self.__cantidad}, "{self.__moneda}")'
# Uso completo
print('=== CLASE DINERO ===\n')
precio_eur = Dinero(100, 'EUR')
precio_usd = Dinero(50, 'USD')
precio_gbp = Dinero(80, 'GBP')
print(f'Precio EUR: {precio_eur}')
print(f'Precio USD: {precio_usd}')
print(f'Precio GBP: {precio_gbp}')
print(f'\n--- Conversión ---')
print(f'{precio_usd} → EUR: {precio_usd.convertir("EUR")}')
print(f'{precio_gbp} → USD: {precio_gbp.convertir("USD")}')
print(f'{precio_eur} → JPY: {precio_eur.convertir("JPY")}')
print(f'\n--- Aritmética ---')
total = precio_eur + precio_usd # USD se convierte a EUR
print(f'{precio_eur} + {precio_usd} = {total}')
descuento = precio_eur * 0.10
print(f'{precio_eur} * 0.10 = {descuento}')
precio_final = precio_eur - descuento
print(f'{precio_eur} - {descuento} = {precio_final}')
cuotas = precio_eur / 3
print(f'{precio_eur} / 3 = {cuotas}')
ratio = precio_eur / precio_usd # cuántos USD caben en 100€
print(f'{precio_eur} / {precio_usd} = {ratio:.2f}x')
print(f'\n--- Comparación ---')
print(f'{precio_eur} == {precio_usd}: {precio_eur == precio_usd}')
print(f'{precio_eur} > {precio_usd}: {precio_eur > precio_usd}')
print(f'\n--- Ordenación ---')
precios = [Dinero(80, 'GBP'), Dinero(50, 'USD'),
Dinero(100, 'EUR'), Dinero(10000, 'JPY')]
for p in sorted(precios):
print(f' {p} = {p.convertir("EUR")}')
=== CLASE DINERO === Precio EUR: 100.00€ Precio USD: 50.00$ Precio GBP: 80.00£ --- Conversión --- 50.00$ → EUR: 45.87€ 80.00£ → USD: 101.51$ 100.00€ → JPY: 16250.00¥ --- Aritmética --- 100.00€ + 50.00$ = 145.87€ 100.00€ * 0.10 = 10.00€ 100.00€ - 10.00€ = 90.00€ 100.00€ / 3 = 33.33€ 100.00€ / 50.00$ = 1.83x --- Comparación --- 100.00€ == 50.00$: False 100.00€ > 50.00$: True --- Ordenación --- 50.00$ = 45.87€ 10000.00¥ = 61.54€ 80.00£ = 93.02€ 100.00€ = 100.00€
Tres detalles clave de Dinero:
Primero — las comparaciones convierten siempre a euros antes de comparar. Así 100€ > 50$ funciona correctamente sin importar la moneda.
Segundo — la suma convierte el operando derecho a la moneda del izquierdo. El resultado siempre tiene la moneda del operando izquierdo.
Tercero — precio / precio devuelve un número sin moneda — el ratio entre dos cantidades. precio / número devuelve Dinero — dividir en cuotas.
Resumen y siguiente paso
En este artículo construiste dos clases con sobrecarga completa de operadores. Racional demuestra el patrón clásico de FP2 con simplificación automática y operadores reflejados no conmutativos. Dinero muestra cómo los operadores pueden incorporar lógica de negocio real, conversión de moneda transparente al usar + o >.
En el siguiente artículo encontrarás ejercicios propuestos con solución.
Python operator overloading — 2 real classes with all operators
Program 1 — Rational Number class
from functools import total_ordering
from math import gcd
@total_ordering
class Rational:
def __init__(self, numerator, denominator=1):
if not isinstance(numerator, int):
raise TypeError('Numerator must be integer')
if not isinstance(denominator, int):
raise TypeError('Denominator must be integer')
if denominator == 0:
raise ZeroDivisionError('Denominator cannot be zero')
if denominator < 0:
numerator = -numerator
denominator = -denominator
common = gcd(abs(numerator), denominator)
self.__num = numerator // common
self.__den = denominator // common
@property
def numerator(self): return self.__num
@property
def denominator(self): return self.__den
@classmethod
def from_float(cls, value, precision=6):
denominator = 10 ** precision
numerator = round(value * denominator)
return cls(numerator, denominator)
def __eq__(self, other):
if isinstance(other, Rational):
return self.__num == other.numerator and self.__den == other.denominator
elif isinstance(other, int):
return self.__num == other and self.__den == 1
return False
def __lt__(self, other):
if isinstance(other, Rational):
return self.__num * other.denominator < other.numerator * self.__den
elif isinstance(other, int):
return self.__num < other * self.__den
return NotImplemented
def __add__(self, other):
if isinstance(other, Rational):
return Rational(self.__num * other.denominator + other.numerator * self.__den,
self.__den * other.denominator)
elif isinstance(other, int):
return self + Rational(other)
return NotImplemented
def __radd__(self, other): return self.__add__(other)
def __sub__(self, other):
if isinstance(other, Rational):
return Rational(self.__num * other.denominator - other.numerator * self.__den,
self.__den * other.denominator)
elif isinstance(other, int): return self - Rational(other)
return NotImplemented
def __rsub__(self, other):
if isinstance(other, int): return Rational(other) - self
return NotImplemented
def __mul__(self, other):
if isinstance(other, Rational):
return Rational(self.__num * other.numerator, self.__den * other.denominator)
elif isinstance(other, int): return Rational(self.__num * other, self.__den)
return NotImplemented
def __rmul__(self, other): return self.__mul__(other)
def __truediv__(self, other):
if isinstance(other, Rational):
if other.numerator == 0: raise ZeroDivisionError('Cannot divide by zero')
return Rational(self.__num * other.denominator, self.__den * other.numerator)
elif isinstance(other, int):
if other == 0: raise ZeroDivisionError('Cannot divide by zero')
return Rational(self.__num, self.__den * other)
return NotImplemented
def __rtruediv__(self, other):
if isinstance(other, int): return Rational(other) / self
return NotImplemented
def __neg__(self): return Rational(-self.__num, self.__den)
def __abs__(self): return Rational(abs(self.__num), self.__den)
def __float__(self): return self.__num / self.__den
def __int__(self): return self.__num // self.__den
def __str__(self): return str(self.__num) if self.__den == 1 else f'{self.__num}/{self.__den}'
def __repr__(self): return f'Rational({self.__num}, {self.__den})'
Three key details: __init__ normalises sign and simplifies with gcd — so Rational(6, 8) and Rational(3, 4) are equal from creation. __rsub__ is not just self.__sub__(other) because subtraction is not commutative. __float__, __int__ and __round__ let you use built-in conversions directly.
Program 2 — Money class with currency conversion
from functools import total_ordering
@total_ordering
class Money:
_rates = {'EUR': 1.0, 'USD': 1.09, 'GBP': 0.86, 'JPY': 162.50, 'CHF': 0.97}
def __init__(self, amount, currency='EUR'):
if not isinstance(amount, (int, float)): raise TypeError('Amount must be numeric')
if amount < 0: raise ValueError('Amount cannot be negative')
if currency not in Money._rates: raise ValueError(f'Unsupported currency: {currency}')
self.__amount = round(float(amount), 2)
self.__currency = currency
@property
def amount(self): return self.__amount
@property
def currency(self): return self.__currency
def convert(self, target_currency):
if target_currency not in Money._rates:
raise ValueError(f'Unsupported currency: {target_currency}')
in_euros = self.__amount / Money._rates[self.__currency]
in_target = in_euros * Money._rates[target_currency]
return Money(round(in_target, 2), target_currency)
def _in_euros(self): return self.__amount / Money._rates[self.__currency]
def __eq__(self, other):
if isinstance(other, Money): return abs(self._in_euros() - other._in_euros()) < 0.001
elif isinstance(other, (int, float)): return self._in_euros() == other
return False
def __lt__(self, other):
if isinstance(other, Money): return self._in_euros() < other._in_euros()
return NotImplemented
def __add__(self, other):
if isinstance(other, Money):
converted = other.convert(self.__currency)
return Money(self.__amount + converted.amount, self.__currency)
elif isinstance(other, (int, float)):
return Money(self.__amount + other, self.__currency)
return NotImplemented
def __radd__(self, other): return self.__add__(other)
def __mul__(self, other):
if isinstance(other, (int, float)): return Money(self.__amount * other, self.__currency)
return NotImplemented
def __rmul__(self, other): return self.__mul__(other)
def __truediv__(self, other):
if isinstance(other, (int, float)):
if other == 0: raise ZeroDivisionError('Cannot divide by zero')
return Money(self.__amount / other, self.__currency)
elif isinstance(other, Money):
converted = other.convert(self.__currency)
return self.__amount / converted.amount
return NotImplemented
def __str__(self):
symbols = {'EUR': '€', 'USD': '$', 'GBP': '£', 'JPY': '¥', 'CHF': 'Fr'}
return f'{self.__amount:.2f}{symbols.get(self.__currency, self.__currency)}'
def __repr__(self): return f'Money({self.__amount}, "{self.__currency}")'
Three key details: comparisons always convert to euros first, 100€ > 50$ works correctly regardless of currency. Addition converts the right operand to the left operand’s currency, result always has the left currency. money / money returns a dimensionless ratio, money / number returns Money.

2 comentarios