Objetos (POO)Herencia en Python

Herencia en Python

Duplicar código es uno de los mayores pecados en el desarrollo de software. Imagina que tienes una clase para gestionar usuarios administradores y otra para clientes generales. Si copias y pegas los atributos comunes como el nombre, el email y los métodos de inicio de sesión en ambas clases, tu base de código se volverá una pesadilla insostenible ante cualquier cambio. Para evitar este desastre, la Programación Orientada a Objetos (POO) nos ofrece una herramienta sumamente potente: la herencia en python.

La herencia te permite crear una clase nueva (clase hija) que copia y amplía de forma automática todos los atributos y comportamientos de una clase existente (clase padre), estableciendo una relación natural del tipo «es un» (por ejemplo: un Desarrollador es un Empleado).

En este tutorial vas a aprender a dominar la herencia simple, el uso avanzado del inicializador super(), la sobrescritura de métodos, y cómo desarmar de una vez por todas el temido «problema del diamante» en la herencia múltiple utilizando el MRO (Method Resolution Order) como un desarrollador senior.

Para ir directos al grano, echa un vistazo al siguiente bloque «spoiler» de código que muestra la sintaxis profesional estándar para heredar y extender clases en Python:

class Empleado:
    def __init__(self, nombre: str, sueldo: float):
        self.nombre = nombre
        self.sueldo = sueldo

    def obtener_detalles(self) -> str:
        return f"{self.nombre} (Sueldo: {self.sueldo}€)"

# La clase Desarrollador hereda directamente de Empleado
class Desarrollador(Empleado):
    def __init__(self, nombre: str, sueldo: float, lenguaje: str):
        # Invocamos al inicializador del padre de forma segura
        super().__init__(nombre, sueldo)
        self.lenguaje = lenguaje  # Añadimos un atributo exclusivo

    # Sobrescribimos el método del padre para extender su comportamiento
    def obtener_detalles(self) -> str:
        detalle_padre = super().obtener_detalles()
        return f"{detalle_padre} - Especialidad: {self.lenguaje}"

# Instanciamos a la clase hija
dev = Desarrollador("Sofía", 45000, "Python")
print(dev.obtener_detalles()) 
# Salida: Sofía (Sueldo: 45000€) - Especialidad: Python

1. Entendiendo la Herencia Simple: Clases Padre y Clases Hija

Para declarar que una clase hereda de otra, basta con pasar el nombre de la clase padre entre paréntesis inmediatamente después de definir la clase hija: class Hija(Padre):. A partir de ese momento, la clase hija tiene acceso nativo a todas las funciones y propiedades del padre.

Veamos un ejemplo práctico y limpio que ilustra cómo una clase hija reutiliza de forma inmediata los métodos de la clase base sin necesidad de escribir una sola línea duplicada:

class Dispositivo:
    def __init__(self, marca: str):
        self.marca = marca

    def encender(self) -> str:
        return f"El dispositivo {self.marca} está encendido."

# Telefono es la clase hija. Hereda de Dispositivo de forma limpia
class Telefono(Dispositivo):
    pass  # No definimos nada nuevo por ahora

# Instanciamos y usamos el método heredado
mi_telefono = Telefono("Xiaomi")
print(mi_telefono.encender()) # Salida: El dispositivo Xiaomi está encendido.

Aunque no hayamos definido el método encender() en la clase Telefono, Python sube por el árbol jerárquico hasta encontrarlo en la clase padre Dispositivo y lo ejecuta perfectamente.


2. La Magia de super() y la Sobrescritura de Métodos

Heredar no significa limitarse a ser una copia exacta de la clase base. Las clases hijas tienen dos superpoderes principales para moldear su propio comportamiento:

A. Sobrescritura (Method Overriding)

Consiste en redefinir en la clase hija un método que ya existía en la clase padre, usando exactamente el mismo nombre. Al invocar dicho método desde un objeto de la clase hija, Python ignorará el del padre y ejecutará la versión especializada de la clase hija.

B. Extensión mediante super()

La función incorporada super() devuelve un objeto temporal de la clase padre, lo cual te permite llamar a sus métodos directamente. Esto es crucial cuando quieres añadir lógica nueva a un método en la clase hija sin descartar por completo el trabajo que ya hacía la clase padre.

La combinación de ambos se aprecia de forma brillante en los inicializadores. Cuando añades atributos específicos a una clase hija, debes llamar a super().__init__() para asegurarte de que el estado básico definido en la clase padre se configure correctamente primero:

class Vehiculo:
    def __init__(self, marca: str):
        self.marca = marca

class Moto(Vehiculo):
    def __init__(self, marca: str, cilindrada: int):
        # 1. Delegamos la inicialización de "marca" al padre
        super().__init__(marca)
        # 2. Inicializamos el estado específico de la hija
        self.cilindrada = cilindrada

mi_moto = Moto("Honda", 600)
print(mi_moto.marca)       # Salida: Honda
print(mi_moto.cilindrada)  # Salida: 600

3. Herencia Múltiple y el MRO: Domina el «Problema del Diamante»

A diferencia de otros lenguajes de programación como Java o C#, Python admite de forma nativa la herencia múltiple, es decir, una clase hija puede heredar directamente de varias clases padres simultáneamente: class Hija(PadreA, PadreB):.

Esto plantea un dilema de diseño clásico en ciencias de la computación conocido como el Problema del Diamante. Si dos clases intermedias heredan de una misma clase base y sobrescriben el mismo método, ¿cuál de las versiones heredará la clase nieta final?

Python resuelve esta jerarquía con una precisión asombrosa mediante el MRO (Method Resolution Order), el cual calcula el árbol de búsqueda utilizando un algoritmo llamado Linealización C3. El MRO garantiza que ninguna clase base se visite dos veces y respeta estrictamente el orden de declaración de izquierda a derecha.

Puedes auditar y comprobar en cualquier momento la ruta de resolución exacta que seguirá Python utilizando el atributo especial __mro__ o llamando al método .mro():

class A:
    def saludar(self):
        return "Hola desde A"

class B(A):
    def saludar(self):
        return "Hola desde B"

class C(A):
    def saludar(self):
        return "Hola desde C"

# Herencia múltiple en forma de diamante: D hereda de B y C
class D(B, C):
    pass

instancia_d = D()
# ¿Quién responde al saludo? B está declarada primero (a la izquierda)
print(instancia_d.saludar()) # Salida: Hola desde B

# Inspeccionamos la ruta de búsqueda oficial
print([cls.__name__ for cls in D.__mro__])
# Salida: ['D', 'B', 'C', 'A', 'object']

Como ves en la salida del MRO, Python busca el método primero en D, luego pasa a B (su primer padre directo), si no estuviera ahí iría a C, luego a la clase base común A y, por último, al tipo raíz object.


4. Herencia vs Composición: La Regla de Oro del Diseño

Un error extremadamente común cuando los desarrolladores descubren la herencia en python es abusar de ella, creando jerarquías profundas e inmanejables de 5 o más niveles de profundidad. Esto provoca un acoplamiento extremo: un cambio mínimo en la clase padre superior puede romper de forma catastrófica decenas de clases hijas.

Para evitarlo, la ingeniería de software profesional nos enseña una regla de oro fundamental: «Prefiere composición sobre herencia siempre que sea posible».

  • Usa Herencia («es un»): Cuando la clase hija sea un subtipo claro y permanente de la clase padre. Por ejemplo, un Gato es un Animal.
  • Usa Composición («tiene un»): Cuando quieras ensamblar funcionalidades complejas a partir de piezas independientes. En lugar de hacer que un Coche herede de Motor, haz que el Coche incorpore un objeto Motor dentro de sus atributos.

Comparemos visualmente la limpieza absoluta de la composición frente a una jerarquía de herencia forzada:

# DISEÑO PREMIUM CON COMPOSICIÓN
class Motor:
    def arrancar(self) -> str:
        return "Motor rugiendo..."

class Coche:
    def __init__(self, marca: str):
        self.marca = marca
        self.motor = Motor() # Composición: Coche "tiene un" Motor

    def arrancar_coche(self) -> str:
        # Delegamos la responsabilidad
        return f"{self.marca}: {self.motor.arrancar()}"

mi_coche = Coche("Ford")
print(mi_coche.arrancar_coche()) # Salida: Ford: Motor rugiendo...

5. Bonus Senior: Interfaces y Clases Abstractas con «abc»

Python no incluye la palabra clave interface que vemos en otros lenguajes. Sin embargo, nos permite lograr exactamente el mismo comportamiento restrictivo y seguro utilizando el módulo integrado abc (Abstract Base Classes).

Una clase abstracta actúa como un contrato formal. Si declaras un método como abstracto utilizando el decorador @abstractmethod, Python impedirá por completo que se puedan crear objetos de esa clase base, y obligará a cualquier clase hija a implementar dicho método de forma explícita si no quiere lanzar un error de ejecución inmediato:

from abc import ABC, abstractmethod

class BaseDatos(ABC): # Hereda de ABC para marcarla como abstracta
    @abstractmethod
    def conectar(self):
        pass

class MySQL(BaseDatos):
    def conectar(self):
        return "Conectado con éxito a MySQL"

# El siguiente código lanzaría un TypeError de inmediato:
# db_base = BaseDatos() # Error: Can't instantiate abstract class...

db_real = MySQL()
print(db_real.conectar()) # Salida: Conectado con éxito a MySQL

6. Tabla Resumen de Conceptos en Herencia

En esta tabla detallada estructuramos los pilares fundamentales que debes dominar para trabajar con herencia sin cometer errores:

TérminoDefinición y PropósitoSintaxis en Python
Herencia SimpleUna clase hija deriva de una única clase padre.class Hija(Padre):
super()Invoca constructores o métodos de la clase base.super().__init__(args)
SobrescrituraReemplaza un método heredado con una versión específica.def saludar(self):
Herencia MúltipleUna clase hija deriva de varios padres a la vez.class Hija(PadreA, PadreB):
MRO (Method Resolution Order)Orden oficial de búsqueda de métodos en herencia.Clase.__mro__
Clase Abstracta (ABC)Molde obligatorio que no se puede instanciar directamente.class Clase(ABC):

Conclusión y Siguientes Pasos

Dominar la herencia en python y entender cuándo delegar tareas al inicializador `super()` te permitirá diseñar estructuras de software extremadamente modulares, limpias y libres de duplicidades. Recuerda siempre mantener tus jerarquías de clases planas y sencillas, recurrir a la composición para inyectar comportamientos flexibles y aprovechar el MRO y las clases abstractas para modelar contratos limpios y seguros. Te aconsejamos consultar la documentación oficial de Python sobre la herencia en Python y experimentar escribiendo tus propios modelos. ¡En la siguiente lección de nuestro Roadmap nos adentraremos en el apasionante mundo de los métodos especiales (Dunder Methods) para darle superpoderes a tus objetos!