Ir al contenido principal

Herencia: clases base y derivadas

Introducción

La herencia es uno de los conceptos fundamentales de la programación orientada a objetos que permite crear nuevas clases basadas en clases existentes. Este mecanismo facilita la reutilización de código, promueve la organización jerárquica y permite modelar relaciones del tipo "es un" entre diferentes entidades. En Python, la herencia se implementa de forma sencilla y potente, permitiendo incluso herencia múltiple. A través de este artículo, exploraremos cómo crear y utilizar clases derivadas, aprovechando y extendiendo la funcionalidad de sus clases base.

Concepto básico de herencia

La herencia permite que una clase (llamada subclase o clase derivada) adquiera automáticamente todas las propiedades y comportamientos de otra clase (llamada superclase o clase base). La subclase puede luego:

  • Usar los métodos y atributos de la clase base tal como están
  • Sobrescribir (redefinir) algunos de ellos
  • Añadir nuevos métodos y atributos propios

Podemos visualizar la herencia como una relación "es un": un perro "es un" animal, un coche deportivo "es un" coche.

Sintaxis para crear una clase derivada

La sintaxis en Python para definir una clase que hereda de otra es muy sencilla:

class ClaseBase:
    # Atributos y métodos de la clase base
    pass

class ClaseDerivada(ClaseBase):
    # Atributos y métodos de la clase derivada
    pass

La clase entre paréntesis es la clase base de la que se hereda.

Ejemplo práctico: Empleados de una empresa

Veamos un ejemplo práctico donde modelamos diferentes tipos de empleados en una empresa:

class Empleado:
    """Clase base para todos los empleados"""
    
    def __init__(self, nombre, dni, salario_base):
        self.nombre = nombre
        self.dni = dni
        self.salario_base = salario_base
    
    def calcular_salario(self):
        return self.salario_base
    
    def mostrar_detalles(self):
        return f"Empleado: {self.nombre}, DNI: {self.dni}, Salario: {self.calcular_salario()}€"


class Programador(Empleado):
    """Clase derivada para programadores"""
    
    def __init__(self, nombre, dni, salario_base, lenguaje, horas_extra=0):
        # Llamamos al constructor de la clase base
        super().__init__(nombre, dni, salario_base)
        # Añadimos atributos propios
        self.lenguaje = lenguaje
        self.horas_extra = horas_extra
    
    def calcular_salario(self):
        # Sobrescribimos el método para incluir pago por horas extra
        return self.salario_base + (self.horas_extra * 20)
    
    def programar(self):
        return f"{self.nombre} está programando en {self.lenguaje}"


class Gerente(Empleado):
    """Clase derivada para gerentes"""
    
    def __init__(self, nombre, dni, salario_base, departamento, bonus=0):
        super().__init__(nombre, dni, salario_base)
        self.departamento = departamento
        self.bonus = bonus
    
    def calcular_salario(self):
        return self.salario_base + self.bonus
    
    def gestionar_equipo(self):
        return f"{self.nombre} está gestionando el departamento de {self.departamento}"

La función super()

La función super() es un componente clave en la herencia. Nos permite llamar a métodos de la clase base desde la clase derivada:

super().__init__(nombre, dni, salario_base)

Es especialmente útil para:

  • Inicializar la parte de los atributos que maneja la clase base
  • Extender la funcionalidad de un método en lugar de reemplazarlo por completo

Uso de las clases

Ahora veamos cómo podemos usar estas clases:

# Creamos instancias de las diferentes clases
empleado_general = Empleado("Laura Pérez", "12345678A", 1500)
programador = Programador("Carlos Gómez", "87654321B", 1800, "Python", 10)
gerente = Gerente("Ana Martínez", "11223344C", 2200, "Desarrollo", 500)

# Accedemos a métodos comunes
print(empleado_general.mostrar_detalles())  # Empleado: Laura Pérez, DNI: 12345678A, Salario: 1500€
print(programador.mostrar_detalles())  # Empleado: Carlos Gómez, DNI: 87654321B, Salario: 2000€
print(gerente.mostrar_detalles())  # Empleado: Ana Martínez, DNI: 11223344C, Salario: 2700€

# Usamos métodos específicos de cada clase derivada
print(programador.programar())  # Carlos Gómez está programando en Python
print(gerente.gestionar_equipo())  # Ana Martínez está gestionando el departamento de Desarrollo

Verificación de herencia

Python proporciona funciones para comprobar las relaciones de herencia:

# isinstance() comprueba si un objeto es instancia de una clase
print(isinstance(programador, Programador))  # True
print(isinstance(programador, Empleado))  # True
print(isinstance(programador, Gerente))  # False

# issubclass() comprueba si una clase es subclase de otra
print(issubclass(Programador, Empleado))  # True
print(issubclass(Gerente, Empleado))  # True
print(issubclass(Programador, Gerente))  # False

Herencia múltiple

Python permite que una clase herede de múltiples clases base, una característica que no todos los lenguajes orientados a objetos ofrecen:

class DispositivoElectronico:
    def __init__(self, marca, modelo):
        self.marca = marca
        self.modelo = modelo
        self.encendido = False
    
    def encender(self):
        self.encendido = True
        return f"{self.marca} {self.modelo} está encendido"
    
    def apagar(self):
        self.encendido = False
        return f"{self.marca} {self.modelo} está apagado"


class DispositivoConectado:
    def __init__(self):
        self.conectado = False
    
    def conectar(self):
        self.conectado = True
        return "Dispositivo conectado a internet"
    
    def desconectar(self):
        self.conectado = False
        return "Dispositivo desconectado de internet"


class Portatil(DispositivoElectronico, DispositivoConectado):
    def __init__(self, marca, modelo, ram):
        DispositivoElectronico.__init__(self, marca, modelo)
        DispositivoConectado.__init__(self)
        self.ram = ram
    
    def describir(self):
        estado_conexion = "conectado" if self.conectado else "no conectado"
        estado_encendido = "encendido" if self.encendido else "apagado"
        return f"Portátil {self.marca} {self.modelo} con {self.ram}GB de RAM, {estado_encendido} y {estado_conexion}"

En este caso, Portatil hereda de dos clases base. Observa cómo debemos llamar explícitamente a los constructores de ambas clases.

El problema del diamante y el MRO (Method Resolution Order)

La herencia múltiple puede llevar a ambigüedades, especialmente en el "problema del diamante", donde una clase hereda de dos clases que tienen un ancestro común:

    A
   / \
  B   C
   \ /
    D

Si B y C sobrescriben un método de A, ¿qué implementación debe usar D? Python resuelve esto mediante el Orden de Resolución de Métodos (MRO), que se puede consultar a través del atributo __mro__:

print(Portatil.__mro__)
# Muestra el orden en que Python busca los métodos

El MRO utiliza un algoritmo llamado C3 que garantiza un orden predecible y que respeta la herencia simple y múltiple.

Resumen

La herencia en Python es un mecanismo potente que facilita la reutilización de código y la estructuración de clases en relaciones jerárquicas naturales. A través de la sintaxis sencilla para crear clases derivadas y el uso de super(), podemos extender y modificar el comportamiento de las clases base. Python proporciona además herramientas para verificar relaciones de herencia (isinstance(), issubclass()) y soporta herencia múltiple, con un sistema sólido para resolver ambigüedades. Dominar este concepto abrirá las puertas al siguiente nivel de programación orientada a objetos: el polimorfismo, que veremos en el próximo artículo del tutorial.