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.
Python inheritance — 3 real class hierarchies with polymorphism
Program 1 — Employee payroll system
from abc import ABC, abstractmethod
class Employee(ABC):
_total = 0
def __init__(self, name, base_salary):
if not name or not isinstance(name, str):
raise ValueError('Invalid name')
if not isinstance(base_salary, (int, float)) or base_salary < 0:
raise ValueError('Invalid salary')
self.__name = name
self.__base_salary = base_salary
Employee._total += 1
@property
def name(self): return self.__name
@property
def base_salary(self): return self.__base_salary
@abstractmethod
def bonus(self): pass
@abstractmethod
def category(self): pass
def total_salary(self):
return self.__base_salary + self.bonus()
def payslip(self):
return (f'--- {self.__name} payslip ---\n'
f'Category: {self.category()}\n'
f'Base salary: {self.__base_salary:.2f}€\n'
f'Bonus: {self.bonus():.2f}€\n'
f'TOTAL: {self.total_salary():.2f}€')
@classmethod
def total_employees(cls): return cls._total
def __str__(self):
return f'{self.__name} ({self.category()}) — {self.total_salary():.2f}€/month'
def __repr__(self):
return f'{self.__class__.__name__}("{self.__name}", {self.__base_salary})'
class Programmer(Employee):
LANGUAGE_BONUS = {'Python': 300, 'Java': 250, 'C': 200}
def __init__(self, name, base_salary, language, years_exp):
super().__init__(name, base_salary)
if language not in self.LANGUAGE_BONUS:
raise ValueError(f'Unknown language: {language}')
self.__language = language
self.__experience = max(0, years_exp)
def bonus(self):
return self.LANGUAGE_BONUS[self.__language] + self.__experience * 50
def category(self):
if self.__experience >= 5: return f'Senior Programmer ({self.__language})'
elif self.__experience >= 2: return f'Mid Programmer ({self.__language})'
else: return f'Junior Programmer ({self.__language})'
class Designer(Employee):
def __init__(self, name, base_salary, tool, projects_month):
super().__init__(name, base_salary)
self.__tool = tool
self.__projects = max(0, projects_month)
def bonus(self): return self.__projects * 150
def category(self): return f'Designer ({self.__tool})'
class Manager(Employee):
def __init__(self, name, base_salary, team):
super().__init__(name, base_salary)
self.__team = list(team)
def bonus(self): return self.base_salary * 0.30 + len(self.__team) * 100
def category(self): return 'Senior Manager' if len(self.__team) >= 5 else 'Manager'
Three key points: Employee is abstract with bonus() and category() abstract. total_salary() and payslip() live in the superclass and use abstract methods — the superclass uses methods that don’t exist in itself yet. __repr__ uses self.__class__.__name__ to get the real class name without hardcoding.
Program 2 — Vehicle hierarchy
from abc import ABC, abstractmethod
class Vehicle(ABC):
def __init__(self, make, model, year, price):
self.__make = make
self.__model = model
self.__year = year
self.__price = price
self.__km = 0
@property
def make(self): return self.__make
@property
def model(self): return self.__model
@property
def km(self): return self.__km
def drive(self, km):
if km <= 0: raise ValueError('km must be positive')
self.__km += km
@abstractmethod
def type(self): pass
@abstractmethod
def cost_per_km(self): pass
def trip_cost(self, km):
return km * self.cost_per_km()
def current_value(self):
depreciation = (2025 - self.__year) * 0.08
return round(self.__price * max(0.1, 1 - depreciation), 2)
def __str__(self):
return f'{self.type()}: {self.__make} {self.__model} ({self.__year}) — {self.__km:.0f} km'
class Petrol(Vehicle):
def __init__(self, make, model, year, price, consumption_l100km):
super().__init__(make, model, year, price)
self.__consumption = consumption_l100km
self.__petrol_price = 1.65
def type(self): return 'Petrol'
def cost_per_km(self): return (self.__consumption / 100) * self.__petrol_price
@property
def range_km(self): return (60 / self.__consumption) * 100
class Electric(Vehicle):
def __init__(self, make, model, year, price, kwh_100km, battery_kwh):
super().__init__(make, model, year, price)
self.__consumption = kwh_100km
self.__battery = battery_kwh
self.__electricity_price = 0.15
def type(self): return 'Electric'
def cost_per_km(self): return (self.__consumption / 100) * self.__electricity_price
@property
def range_km(self): return (self.__battery / self.__consumption) * 100
class Hybrid(Petrol):
def __init__(self, make, model, year, price, consumption, electric_saving):
super().__init__(make, model, year, price, consumption)
self.__saving = electric_saving
def type(self): return 'Hybrid'
def cost_per_km(self): return super().cost_per_km() * (1 - self.__saving)
Hybrid inherits from Petrol which inherits from Vehicle, two-level inheritance. Its cost_per_km() calls super().cost_per_km() (Petrol’s version) and applies electric saving on top.
Program 3 — Store with specific product types
from abc import ABC, abstractmethod
class Product(ABC):
_vat = 0.21
def __init__(self, name, base_price):
self.__name = name
self.__base_price = base_price
@property
def name(self): return self.__name
@property
def base_price(self): return self.__base_price
@abstractmethod
def type(self): pass
@abstractmethod
def final_price(self): pass
@abstractmethod
def extra_info(self): pass
def price_with_vat(self):
return round(self.final_price() * (1 + Product._vat), 2)
def product_sheet(self):
return (f'[{self.type()}] {self.__name}\n'
f' Base price: {self.__base_price:.2f}€\n'
f' Final price: {self.final_price():.2f}€\n'
f' With VAT: {self.price_with_vat():.2f}€\n'
f' {self.extra_info()}')
class PhysicalProduct(Product):
def __init__(self, name, base_price, weight_kg, stock):
super().__init__(name, base_price)
self.__weight = weight_kg
self.__stock = stock
def type(self): return 'Physical'
def final_price(self):
shipping = 3.99 if self.__weight <= 1 else self.__weight * 2.5
return self.base_price + shipping
def extra_info(self): return f'Weight: {self.__weight}kg | Stock: {self.__stock}'
def sell(self, qty=1):
if qty > self.__stock: raise ValueError(f'Insufficient stock: {self.__stock}')
self.__stock -= qty
class DigitalProduct(Product):
def __init__(self, name, base_price, format, size_mb):
super().__init__(name, base_price)
self.__format = format
self.__size = size_mb
def type(self): return 'Digital'
def final_price(self): return self.base_price
def extra_info(self): return f'Format: {self.__format} | Size: {self.__size}MB'
class Subscription(Product):
PERIODS = {'monthly': 1, 'quarterly': 3, 'annual': 12}
def __init__(self, name, price_month, period, max_users):
if period not in self.PERIODS:
raise ValueError(f'Invalid period: {period}')
super().__init__(name, price_month)
self.__period = period
self.__months = self.PERIODS[period]
self.__users = max_users
def type(self): return f'{self.__period.capitalize()} subscription'
def final_price(self):
discount = {1: 0, 3: 0.05, 12: 0.20}[self.__months]
return round(self.base_price * self.__months * (1 - discount), 2)
def extra_info(self): return f'Duration: {self.__months} month(s) | Users: {self.__users}'
product_sheet() and price_with_vat() live in Product and call final_price(), type() and extra_info(), abstract methods implemented by each subclass. The superclass orchestrates behaviour without knowing the details of each product type.

Un comentario