herencia en Python práctica super override clases abstractas

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.

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:

PrimeroEmpleado es abstracta con dos métodos abstractos: bonus() y categoria(). Cada subclase los implementa a su manera — eso es polimorfismo.

Segundosalario_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.

Publicaciones Similares

Un comentario

Deja una respuesta

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