Saltar al contenido

# programacion-orientada-a-objetos-en-python

Programación Orientada a Objetos en Python

Volver a la portada del curso
Notebook

Uso de la abstracción en diseños

Sección 4 · Encapsulación y Abstracción

Uso de la abstracción en diseños

En esta práctica proteges el estado de los productos de PyShop y diseñas la tienda alrededor de abstracciones: el carrito trabaja con «un producto» y con «un descuento» sin saber qué clase concreta recibe. Descarga el notebook adjunto y ejecútalo en orden; todo usa la biblioteca estándar (abc, dataclasses y typing).

1. Encapsulación con property

Python no tiene atributos privados. La convención _stock avisa que es de uso interno, y @property controla el acceso real:

@property
def precio(self):
    return self._precio

@precio.setter
def precio(self, valor):
    if valor <= 0:
        raise PrecioInvalidoError(f"Precio inválido para {self.nombre}: {valor}")
    self._precio = round(valor, 2)

@property
def stock(self):          # sin setter: solo lectura
    return self._stock

Como __init__ asigna self.precio = precio, la validación también corre al crear el objeto. PrecioInvalidoError hereda de ValueError, así que except ValueError la atrapa. Y taza.stock = 1000 lanza AttributeError: property 'stock' of 'ProductoFisico' object has no setter: el stock solo cambia con reponer() y retirar().

2. Name mangling

Un atributo self.__fondo dentro de la clase Caja se guarda como _Caja__fondo. No es seguridad, porque se puede leer con ese nombre. Sirve para que una subclase con su propio __fondo no pise el de la base: en el notebook, vars(caja) muestra {'_Caja__fondo': 500, '_CajaExpress__fondo': 50}.

3. Clases abstractas

Producto hereda de ABC y marca costo_envio con @abstractmethod. Mientras falte implementar un método abstracto, la clase no se puede instanciar:

try:
    Producto("X", 10)
except TypeError as e:
    print(e)
# Can't instantiate abstract class Producto without an
# implementation for abstract method 'costo_envio'

El error aparece al crear el objeto, no al definir la clase ni al llamar el método. ProductoFisico cobra 49 + 15 * peso_kg * cantidad y ProductoDigital cobra 0: dos tazas de 0.4 kg pagan 61.0 de envío y un teclado de 1.2 kg paga 67.0.

4. Polimorfismo con descuentos

Descuento es otra clase abstracta con un solo método, aplicar(subtotal). El carrito llama self.descuento.aplicar(...) sin preguntar el tipo:

def total(self):
    rebaja = self.descuento.aplicar(self.subtotal()) if self.descuento else 0.0
    return round(self.subtotal() - rebaja + self.envio(), 2)

El carrito de Ana (2 tazas, 1 teclado y 1 ebook) tiene subtotal 1496.0, envío 128.0 y total 1624.0. Con DescuentoPorcentaje(10) baja a 1474.4. Con DescuentoFijo(200, minimo=1500) se queda en 1624.0, porque 1496.0 no alcanza el mínimo. Agregar un descuento nuevo no obliga a tocar Carrito.

5. Protocol: abstracción sin herencia

Con ABC una clase cumple el contrato porque hereda de él. Con typing.Protocol basta con que tenga los métodos. CuponNavidad no hereda de nada, pero tiene aplicar, así que el carrito lo usa sin problema (total 1474.0 con un cupón de 150). Con @runtime_checkable, isinstance(cupon, Aplicable) da True, mientras que isinstance(cupon, Descuento) da False.

Trampas comunes

  • Creer que __atributo es privado. Solo cambia el nombre; la encapsulación real la da @property.
  • Validar solo en el setter y asignar self._precio en __init__. Así te saltas la validación; asigna self.precio.
  • Esperar que @runtime_checkable revise firmas. Solo comprueba que el atributo exista: una clase con aplicar = "texto" también pasa el isinstance.
  • Llenar el carrito de if isinstance(...) para cada tipo de descuento. Si cada clase implementa aplicar, el carrito no necesita saber cuál recibe.

Qué hacer con el notebook

Ejecuta las seis secciones y después resuelve los dos ejercicios. En el primero creas DescuentoPorPiezas sin tocar Carrito: recibe el carrito en su constructor y aplica un porcentaje si len(carrito) alcanza el mínimo. Con 4 piezas y 5 %, el descuento es 74.8 y el total 1549.2; con un mínimo de 5 piezas no aplica y el total sigue en 1624.0. En el segundo conviertes peso_kg en una propiedad validada. Escribe tu versión antes de leer la solución y verifica que pase los mismos assert.

Recursos