# programacion-orientada-a-objetos-en-python
Programación Orientada a Objetos en Python
Volver a la portada del cursoPolimorfismo práctico
Sección 3 · Herencia y Polimorfismo
Polimorfismo práctico
El carrito de Ana tiene dos tazas, un teclado y un ebook, y su envío total es 128.0. Lo interesante es que el método envio de Carrito no contiene ningún if sobre el tipo de producto: no sabe si está sumando algo que pesa 1.2 kg o un archivo. Esa ignorancia deliberada es el polimorfismo, y es lo que permite agregar tipos nuevos a la tienda sin reabrir el código que ya funciona.
Un mensaje, varias respuestas
Polimorfismo significa que objetos de clases distintas responden al mismo método, cada uno a su manera. En PyShop, ProductoFisico.costo_envio calcula $49 + 15 \cdot \text{peso} \cdot \text{cantidad}$ y ProductoDigital.costo_envio devuelve 0.0. El carrito solo envía el mensaje:
carrito = Carrito("Ana")
carrito.agregar(taza, 2)
carrito.agregar(teclado, 1)
carrito.agregar(ebook, 1)
for linea in carrito:
print(linea.producto.nombre, linea.producto.costo_envio(linea.cantidad))
# Taza 61.0 | Teclado 67.0 | Ebook de Python 0.0
print(carrito.envio()) # 128.0
Python decide qué costo_envio ejecutar en tiempo de ejecución, buscando el método en la clase del objeto concreto. Quien llama no necesita saber cuál es.
Duck typing: importa lo que hace, no lo que es
En Python el polimorfismo no exige herencia. Si un objeto tiene los métodos que el código usa, funciona: «si camina como pato y grazna como pato, es un pato». Una envoltura de regalo no es un Producto, pero sabe cotizarse:
class Envoltura:
nombre = "Envoltura de regalo"
def subtotal(self, cantidad):
return 35.0 * cantidad
def costo_envio(self, cantidad):
return 0.0
carrito.agregar(Envoltura(), 1)
print(len(carrito), carrito.subtotal(), carrito.envio(), carrito.total())
# 5 1531.0 128.0 1659.0
print(isinstance(Envoltura(), Producto)) # False, y aun así funciona
El contrato implícito es «tener subtotal y costo_envio». Es flexible, pero ese contrato no está escrito en ningún lado; en la lección de abstracción verás cómo hacerlo explícito con abc y typing.Protocol.
Los métodos especiales también son polimorfismo
len, str y el operador + funcionan con tipos muy distintos porque cada clase implementa su propio método especial. len(x) llama a x.__len__(), str(x) a x.__str__() y a + b a a.__add__(b):
print(len(carrito), len("PyShop"), len([taza, teclado])) # 5 6 2
print(str(taza)) # Taza — $149.00 MXN
print(3 + 4, "Py" + "Shop", [1] + [2]) # 7 PyShop [1, 2]
print((3).__add__(4), "Py".__add__("Shop")) # 7 PyShop
Como Carrito define __len__ contando piezas, len(carrito) se integra con el resto del lenguaje sin que nadie aprenda un método nuevo.
La alternativa que conviene evitar
Sin polimorfismo, el cálculo del envío termina en una cadena de isinstance:
def envio_con_ifs(producto, cantidad):
if isinstance(producto, ProductoDigital):
return 0.0
elif isinstance(producto, ProductoFisico):
return round(49 + 15 * producto.peso_kg * cantidad, 2)
raise TypeError(f"No sé enviar {type(producto).__name__}")
print(envio_con_ifs(taza, 2)) # 61.0
envio_con_ifs(Envoltura(), 1) # TypeError: No sé enviar Envoltura
Cada tipo nuevo obliga a editar esta función, y la lógica de un producto queda repartida fuera de su clase. La implementación de referencia de PyShop evita esa cadena en Carrito.confirmar: en lugar de preguntar si el producto es digital, le pregunta al propio objeto con producto.hay_stock(cantidad). Producto compara contra su stock y ProductoDigital sobrescribe el método para responder siempre True. El carrito no conoce ningún tipo concreto. Eso sí, la envoltura, que sirve para cotizar, falla al confirmar con AttributeError: 'Envoltura' object has no attribute 'hay_stock': su contrato está incompleto.
Composición antes que herencia
La herencia expresa «es un»: un ProductoFisico es un Producto. La composición expresa «tiene»: el Carrito tiene líneas. Heredar de list parece cómodo, pero un CarritoLista(list) con dos líneas devuelve len igual a 2 (líneas), mientras que el Carrito de PyShop devuelve 3 (piezas), y además expone clear, reverse y todo lo demás de la lista. Con composición decides exactamente qué ofrece la clase.
Herencia múltiple, con cuidado
Python permite heredar de varias clases. El uso más sano es el mixin: una clase pequeña que aporta un comportamiento puntual.
class ExportableMixin:
def a_dict(self):
return {"sku": self.sku, "nombre": self.nombre, "precio": self.precio}
class ProductoFisicoExportable(ExportableMixin, ProductoFisico):
pass
print([k.__name__ for k in ProductoFisicoExportable.__mro__])
# ['ProductoFisicoExportable', 'ExportableMixin', 'ProductoFisico', 'Producto', 'ABC', 'object']
El orden de resolución de métodos (MRO) decide qué clase gana cuando dos definen el mismo nombre: se recorre de izquierda a derecha. Si declaras un orden imposible, como class Malo(Producto, ProductoFisico), Python lo rechaza con TypeError: Cannot create a consistent method resolution order (MRO) for bases Producto, ProductoFisico.
Trampas comunes
- Contratos de duck typing incompletos. La envoltura cumple para cotizar pero no para confirmar. Revisa todos los métodos que el código realmente usa.
- Firmas distintas entre subclases. Si una subclase define
costo_envio(self)sincantidad, el carrito falla solo con ese tipo. - Heredar para reutilizar dos líneas. Si no se cumple «es un», usa composición.
- Mixins con estado propio y
__init__. Complican el MRO; mantenlos sin estado.
Cierre
Cuando encuentres un if isinstance que elige qué hacer según el tipo, pregúntate si esa decisión puede vivir como un método en cada clase. Si puede, mueve la lógica y deja que el objeto responda.