sobrecarga de operadores en Python práctica racional dinero

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.

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.

Terceroprecio / 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.

Publicaciones Similares

2 comentarios

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *