Herencia en Python — 3 jerarquías de clases reales con polimorfismo
La herencia en Python práctica real es lo que toca ahora. En el artículo anterior vimos la teoría. Ahora construimos tres jerarquías de clases reales, cada una demuestra un aspecto diferente de la herencia y el polimorfismo. Usa pythontutor.com para visualizar el funcionamiento de lo practicado.
Tabla de Contenidos
Herencia en Python práctica — Programa 1: Sistema de empleados con nómina y bonificaciones
from abc import ABC, abstractmethod
class Empleado(ABC):
_total_empleados = 0
def __init__(self, nombre, salario_base):
if not nombre or not isinstance(nombre, str):
raise ValueError('Nombre no válido')
if not isinstance(salario_base, (int, float)) or salario_base < 0:
raise ValueError('Salario base no válido')
self.__nombre = nombre
self.__salario_base = salario_base
Empleado._total_empleados += 1
@property
def nombre(self):
return self.__nombre
@property
def salario_base(self):
return self.__salario_base
@abstractmethod
def bonus(self):
pass
@abstractmethod
def categoria(self):
pass
def salario_total(self):
return self.__salario_base + self.bonus()
def nomina(self):
return (f'--- Nómina de {self.__nombre} ---\n'
f'Categoría: {self.categoria()}\n'
f'Salario base: {self.__salario_base:.2f}€\n'
f'Bonus: {self.bonus():.2f}€\n'
f'TOTAL: {self.salario_total():.2f}€')
@classmethod
def total_empleados(cls):
return cls._total_empleados
def __str__(self):
return f'{self.__nombre} ({self.categoria()}) — {self.salario_total():.2f}€/mes'
def __repr__(self):
return f'{self.__class__.__name__}("{self.__nombre}", {self.__salario_base})'
class Programador(Empleado):
BONUS_POR_LENGUAJE = {
'Python': 300, 'Java': 250, 'C': 200, 'JavaScript': 220
}
def __init__(self, nombre, salario_base, lenguaje, anios_experiencia):
super().__init__(nombre, salario_base)
if lenguaje not in self.BONUS_POR_LENGUAJE:
raise ValueError(f'Lenguaje no reconocido: {lenguaje}')
self.__lenguaje = lenguaje
self.__experiencia = max(0, anios_experiencia)
def bonus(self):
bonus_lenguaje = self.BONUS_POR_LENGUAJE[self.__lenguaje]
bonus_experiencia = self.__experiencia * 50
return bonus_lenguaje + bonus_experiencia
def categoria(self):
if self.__experiencia >= 5:
return f'Programador Senior ({self.__lenguaje})'
elif self.__experiencia >= 2:
return f'Programador Mid ({self.__lenguaje})'
else:
return f'Programador Junior ({self.__lenguaje})'
def __str__(self):
base = super().__str__()
return f'{base} | {self.__experiencia} años exp.'
class Disenador(Empleado):
def __init__(self, nombre, salario_base, herramienta, proyectos_mes):
super().__init__(nombre, salario_base)
self.__herramienta = herramienta
self.__proyectos = max(0, proyectos_mes)
def bonus(self):
return self.__proyectos * 150
def categoria(self):
return f'Diseñador ({self.__herramienta})'
class Manager(Empleado):
def __init__(self, nombre, salario_base, equipo):
super().__init__(nombre, salario_base)
self.__equipo = list(equipo)
def bonus(self):
return self.salario_base * 0.30 + len(self.__equipo) * 100
def categoria(self):
tamanio = len(self.__equipo)
if tamanio >= 5:
return 'Manager Senior'
return 'Manager'
def añadir_miembro(self, nombre):
self.__equipo.append(nombre)
@property
def equipo(self):
return list(self.__equipo)
# Uso
p1 = Programador('Sergio', 2000, 'Python', 3)
p2 = Programador('Ana', 1800, 'Java', 1)
d1 = Disenador('María', 1900, 'Figma', 4)
m1 = Manager('Juan', 3000, ['Sergio', 'Ana', 'María'])
print(p1.nomina())
print()
print(p2.nomina())
print()
print(d1.nomina())
print()
print(m1.nomina())
print()
print(f'Total empleados: {Empleado.total_empleados()}')
--- Nómina de Sergio --- Categoría: Programador Mid (Python) Salario base: 2000.00€ Bonus: 450.00€ TOTAL: 2450.00€ --- Nómina de Ana --- Categoría: Programador Junior (Java) Salario base: 1800.00€ Bonus: 250.00€ TOTAL: 2050.00€ --- Nómina de María --- Categoría: Diseñador (Figma) Salario base: 1900.00€ Bonus: 600.00€ TOTAL: 2500.00€ --- Nómina de Juan --- Categoría: Manager Salario base: 3000.00€ Bonus: 1200.00€ TOTAL: 4200.00€ Total empleados: 4
Fíjate en tres cosas clave de este programa:
Primero — Empleado es abstracta con dos métodos abstractos: bonus() y categoria(). Cada subclase los implementa a su manera — eso es polimorfismo.
Segundo — salario_total() y nomina() están en la superclase y usan self.bonus() y self.categoria() — métodos que serán definidos por las subclases. La superclase usa métodos que aún no existen en ella misma.
Tercero — __repr__ usa self.__class__.__name__ para obtener el nombre de la clase real del objeto (Programador, Disenador o Manager) sin hardcodear nada.
Herencia en Python práctica — Programa 2: Jerarquía de vehículos con comportamiento diferente
from abc import ABC, abstractmethod
class Vehiculo(ABC):
def __init__(self, marca, modelo, anio, precio):
self.__marca = marca
self.__modelo = modelo
self.__anio = anio
self.__precio = precio
self.__km = 0
@property
def marca(self):
return self.__marca
@property
def modelo(self):
return self.__modelo
@property
def anio(self):
return self.__anio
@property
def precio(self):
return self.__precio
@property
def km(self):
return self.__km
def conducir(self, km):
if km <= 0:
raise ValueError('Los km deben ser positivos')
self.__km += km
@abstractmethod
def tipo(self):
pass
@abstractmethod
def coste_por_km(self):
pass
def coste_viaje(self, km):
return km * self.coste_por_km()
def valor_actual(self):
depreciacion = (2025 - self.__anio) * 0.08
factor = max(0.1, 1 - depreciacion)
return round(self.__precio * factor, 2)
def __str__(self):
return (f'{self.tipo()}: {self.__marca} {self.__modelo} '
f'({self.__anio}) — {self.__km:.0f} km')
def __repr__(self):
return (f'{self.__class__.__name__}("{self.__marca}", '
f'"{self.__modelo}", {self.__anio}, {self.__precio})')
class Gasolina(Vehiculo):
def __init__(self, marca, modelo, anio, precio, consumo_l100km):
super().__init__(marca, modelo, anio, precio)
self.__consumo = consumo_l100km
self.__precio_gasolina = 1.65
def tipo(self):
return 'Gasolina'
def coste_por_km(self):
return (self.__consumo / 100) * self.__precio_gasolina
@property
def autonomia(self):
return (60 / self.__consumo) * 100 # con 60L de depósito
class Electrico(Vehiculo):
def __init__(self, marca, modelo, anio, precio, kwh_100km, bateria_kwh):
super().__init__(marca, modelo, anio, precio)
self.__consumo_kwh = kwh_100km
self.__bateria = bateria_kwh
self.__precio_kwh = 0.15
def tipo(self):
return 'Eléctrico'
def coste_por_km(self):
return (self.__consumo_kwh / 100) * self.__precio_kwh
@property
def autonomia(self):
return (self.__bateria / self.__consumo_kwh) * 100
class Hibrido(Gasolina):
def __init__(self, marca, modelo, anio, precio, consumo_l100km, ahorro_electrico):
super().__init__(marca, modelo, anio, precio, consumo_l100km)
self.__ahorro = ahorro_electrico # porcentaje de ahorro 0-1
def tipo(self):
return 'Híbrido'
def coste_por_km(self):
coste_gasolina = super().coste_por_km()
return coste_gasolina * (1 - self.__ahorro)
# Uso
g = Gasolina('Toyota', 'Corolla', 2020, 22000, 6.5)
e = Electrico('Tesla', 'Model 3', 2022, 45000, 15, 75)
h = Hibrido('Toyota', 'Prius', 2021, 30000, 4.5, 0.35)
vehiculos = [g, e, h]
for v in vehiculos:
print(v)
print(f' Coste/km: {v.coste_por_km():.4f}€')
print(f' Coste 500km: {v.coste_viaje(500):.2f}€')
print(f' Autonomía: {v.autonomia:.0f} km')
print(f' Valor actual: {v.valor_actual():.2f}€')
print()
g.conducir(15000)
print(g)
Gasolina: Toyota Corolla (2020) — 0 km Coste/km: 0.1073€ Coste 500km: 53.63€ Autonomía: 923 km Valor actual: 17600.00€ Eléctrico: Tesla Model 3 (2022) — 0 km Coste/km: 0.0225€ Coste 500km: 11.25€ Autonomía: 500 km Valor actual: 38250.00€ Híbrido: Toyota Prius (2021) — 0 km Coste/km: 0.0483€ Coste 500km: 24.15€ Autonomía: 1333 km Valor actual: 24000.00€ Gasolina: Toyota Corolla (2020) — 15000 km
Hibrido hereda de Gasolina que hereda de Vehiculo, herencia en dos niveles. Su coste_por_km() llama a super().coste_por_km() que es el de Gasolina, y aplica el ahorro eléctrico encima. Eso es extender el comportamiento heredado.
Herencia en Python práctica — Programa 3: Tienda con productos específicos
from abc import ABC, abstractmethod
class Producto(ABC):
_iva = 0.21
def __init__(self, nombre, precio_base):
self.__nombre = nombre
self.__precio_base = precio_base
@property
def nombre(self):
return self.__nombre
@property
def precio_base(self):
return self.__precio_base
@abstractmethod
def tipo(self):
pass
@abstractmethod
def precio_final(self):
pass
@abstractmethod
def descripcion_extra(self):
pass
def precio_con_iva(self):
return round(self.precio_final() * (1 + Producto._iva), 2)
def ficha(self):
return (f'[{self.tipo()}] {self.__nombre}\n'
f' Precio base: {self.__precio_base:.2f}€\n'
f' Precio final: {self.precio_final():.2f}€\n'
f' Con IVA: {self.precio_con_iva():.2f}€\n'
f' {self.descripcion_extra()}')
def __str__(self):
return f'{self.__nombre} ({self.tipo()}) — {self.precio_con_iva():.2f}€'
class ProductoFisico(Producto):
def __init__(self, nombre, precio_base, peso_kg, stock):
super().__init__(nombre, precio_base)
self.__peso = peso_kg
self.__stock = stock
def tipo(self):
return 'Físico'
def precio_final(self):
gastos_envio = 3.99 if self.__peso <= 1 else self.__peso * 2.5
return self.precio_base + gastos_envio
def descripcion_extra(self):
return f'Peso: {self.__peso}kg | Stock: {self.__stock} uds'
def vender(self, cantidad=1):
if cantidad > self.__stock:
raise ValueError(f'Stock insuficiente: {self.__stock} uds')
self.__stock -= cantidad
class ProductoDigital(Producto):
def __init__(self, nombre, precio_base, formato, tamanio_mb):
super().__init__(nombre, precio_base)
self.__formato = formato
self.__tamanio = tamanio_mb
def tipo(self):
return 'Digital'
def precio_final(self):
return self.precio_base # sin gastos de envío
def descripcion_extra(self):
return f'Formato: {self.__formato} | Tamaño: {self.__tamanio}MB'
class Suscripcion(Producto):
PERIODOS = {'mensual': 1, 'trimestral': 3, 'anual': 12}
def __init__(self, nombre, precio_mes, periodo, usuarios_max):
if periodo not in self.PERIODOS:
raise ValueError(f'Periodo no válido: {periodo}')
super().__init__(nombre, precio_mes)
self.__periodo = periodo
self.__meses = self.PERIODOS[periodo]
self.__usuarios = usuarios_max
def tipo(self):
return f'Suscripción {self.__periodo}'
def precio_final(self):
descuento = 0
if self.__meses == 3:
descuento = 0.05
elif self.__meses == 12:
descuento = 0.20
return round(self.precio_base * self.__meses * (1 - descuento), 2)
def descripcion_extra(self):
return f'Duración: {self.__meses} mes/es | Usuarios: {self.__usuarios}'
# Tienda
print('=== CATÁLOGO DE PRODUCTOS ===\n')
p1 = ProductoFisico('Teclado Mecánico', 89.99, 1.2, 15)
p2 = ProductoFisico('Monitor 27"', 299.99, 5.5, 8)
p3 = ProductoDigital('Python Avanzado PDF', 24.99, 'PDF', 45)
p4 = ProductoDigital('Pack Iconos SVG', 14.99, 'ZIP', 120)
p5 = Suscripcion('Suite Diseño Pro', 29.99, 'mensual', 1)
p6 = Suscripcion('Suite Diseño Pro', 29.99, 'anual', 5)
productos = [p1, p2, p3, p4, p5, p6]
for prod in productos:
print(prod.ficha())
print()
# Venta
print('--- Procesando venta ---')
p1.vender(2)
print(f'{p1.nombre} vendido x2')
try:
p3.vender(1)
except AttributeError:
print('Los productos digitales no tienen stock limitado')
=== CATÁLOGO DE PRODUCTOS === [Físico] Teclado Mecánico Precio base: 89.99€ Precio final: 92.99€ Con IVA: 112.52€ Peso: 1.2kg | Stock: 15 uds [Digital] Python Avanzado PDF Precio base: 24.99€ Precio final: 24.99€ Con IVA: 30.24€ Formato: PDF | Tamaño: 45MB [Suscripción anual] Suite Diseño Pro Precio base: 29.99€ Precio final: 287.90€ Con IVA: 348.36€ Duración: 12 mes/es | Usuarios: 5
ficha() y precio_con_iva() están en Producto y usan precio_final(), tipo() y descripcion_extra(), métodos abstractos implementados por cada subclase. La superclase orquesta el comportamiento sin conocer los detalles de cada tipo de producto.
Resumen y siguiente paso
En este artículo construiste tres jerarquías de clases reales. El sistema de empleados muestra cómo la superclase puede usar métodos abstractos que implementan las subclases. Los vehículos demuestran herencia en dos niveles con Hibrido → Gasolina → Vehiculo. La tienda muestra cómo precio_con_iva() funciona igual para todos los tipos sin saber cómo calculan su precio_final().
En el siguiente artículo encontrarás ejercicios propuestos con solución.

Un comentario