Fundamentos

Tutorial Fundamentos: Herencia

Python: Aprende el concepto de herencia y cómo reutilizar y extender clases para promover un código estructurado y eficiente.

Aprende Fundamentos GRATIS y certifícate

Concepto de herencia: reutilización y extensión de clases

La herencia es un principio fundamental en la programación orientada a objetos que permite crear nuevas clases basadas en clases existentes. A través de ella, una clase derivada reutiliza atributos y métodos de una clase base, posibilitando la extensión y especialización del comportamiento sin necesidad de reinventar el código.

En Python, la herencia se implementa indicando la clase base entre paréntesis al definir la clase derivada. Esto facilita la creación de jerarquías de clases donde las subclases heredan propiedades y comportamientos de sus superclases. Por ejemplo:

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

    def avanzar(self):
        print(f"El vehículo {self.marca} avanza.")

class Coche(Vehiculo):
    def tocar_claxon(self):
        print(f"El coche {self.marca} toca el claxon.")

En este ejemplo, Coche extiende a Vehiculo, heredando sus atributos y métodos. La clase Coche puede usar el método avanzar y también añadir funcionalidades propias como tocar_claxon. Esto demuestra cómo la herencia promueve la extensibilidad de las clases.

Además, las clases derivadas pueden sobrescribir métodos de la clase base para modificar o mejorar su comportamiento. Utilizando la función super(), es posible llamar al método original y luego ampliar su funcionalidad:

class Coche(Vehiculo):
    def avanzar(self):
        super().avanzar()
        print(f"El coche {self.marca} avanza más rápido.")

Aquí, Coche redefine el método avanzar, pero utiliza super().avanzar() para mantener el comportamiento original antes de agregar modificaciones. Este enfoque es esencial para aprovechar la reutilización del código y mantener una arquitectura coherente.

La herencia también facilita la creación de relaciones jerárquicas entre clases, reflejando conceptos del mundo real. Por ejemplo, si tenemos una clase Animal y subclases como Perro y Gato, podemos modelar comportamientos y características comunes en la superclase y especificar detalles en las subclases.

Es importante usar la herencia de manera adecuada para evitar una dependencia excesiva entre clases y promover el polimorfismo, permitiendo que objetos de diferentes clases puedan ser tratados de forma uniforme. La herencia bien aplicada mejora la modularidad y la legibilidad del código, facilitando su mantenimiento y evolución.

Sintaxis y uso de herencia en diferentes lenguajes

La herencia es un concepto central en la programación orientada a objetos, presente en diversos lenguajes, aunque cada uno implementa su sintaxis de manera particular. A pesar de las diferencias sintácticas, el objetivo común es permitir que una clase derivada herede atributos y métodos de una clase base, fomentando la reutilización y extensión del código.

En lenguajes como Java, la herencia se especifica utilizando la palabra clave extends. Por ejemplo, public class Coche extends Vehiculo indica que la clase Coche hereda de Vehiculo. En C++, se utiliza el símbolo : seguido del especificador de acceso y el nombre de la clase base: class Coche : public Vehiculo. A diferencia de estos, Python ofrece una sintaxis más sencilla y flexible.

En Python, la herencia se indica colocando el nombre de la clase base entre paréntesis al definir la clase derivada:

class Vehiculo:
    def desplazarse(self):
        print("El vehículo se mueve.")

class Coche(Vehiculo):
    def desplazarse(self):
        print("El coche avanza en la carretera.")

En este ejemplo, Coche hereda de Vehiculo y redefine el método desplazarse. La simplicidad de la sintaxis en Python facilita la creación de jerarquías de clases y la sobrescritura de métodos para personalizar el comportamiento de las subclases.

Además, Python soporta la herencia múltiple, permitiendo que una clase derive de más de una clase base. Esto se logra listando las clases base separadas por comas:

class Electricidad:
    def encender(self):
        print("El dispositivo se enciende eléctricamente.")

class Bateria:
    def cargar(self):
        print("La batería se está cargando.")

class CocheElectrico(Electricidad, Bateria):
    pass

auto = CocheElectrico()
auto.encender()
auto.cargar()

Aquí, CocheElectrico hereda de Electricidad y Bateria, obteniendo acceso a los métodos de ambas clases. La herencia múltiple en Python proporciona una gran flexibilidad, aunque requiere manejar con cuidado posibles conflictos en los nombres de métodos.

En otros lenguajes, la herencia múltiple puede estar restringida o implementada de manera diferente. Por ejemplo, Java no permite herencia múltiple de clases, pero sí permite implementar múltiples interfaces. En C#, se utiliza un enfoque similar al de Java, donde una clase puede heredar de una clase base y además implementar interfaces.

La manera de invocar al constructor de la clase base también varía entre lenguajes. En Python, se utiliza la función super() dentro del constructor de la subclase:

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

class Coche(Vehiculo):
    def __init__(self, marca, modelo):
        super().__init__(marca)
        self.modelo = modelo

La llamada a super().__init__(marca) permite inicializar los atributos heredados de Vehiculo. En C++, se usan inicializadores de lista en el constructor de la subclase, y en Java, se utiliza la palabra clave super seguida de los parámetros correspondientes.

La sobrescritura de métodos es una práctica común en la herencia para modificar o ampliar el comportamiento de los métodos heredados. En Python, simplemente se redefine el método en la subclase con el mismo nombre:

class Vehiculo:
    def frenar(self):
        print("El vehículo está frenando.")

class Bicicleta(Vehiculo):
    def frenar(self):
        print("La bicicleta reduce su velocidad.")

Cuando se invoca frenar en una instancia de Bicicleta, se ejecuta la versión redefinida en la subclase. En lenguajes como Java, se utiliza la anotación @Override para indicar explícitamente la sobrescritura.

Es relevante mencionar que en algunos lenguajes se requieren especificadores de acceso para controlar la visibilidad de los métodos y atributos heredados. En C++, por ejemplo, se utiliza public, protected o private al heredar: class Coche : public Vehiculo. En Python, los atributos y métodos son públicos por defecto, aunque se pueden usar convenciones de nomenclatura para indicar que un atributo es privado, precediendo su nombre con doble guión bajo __.

La herencia en Python también permite utilizar funciones integradas como isinstance() y issubclass() para verificar relaciones entre objetos y clases:

print(isinstance(auto, CocheElectrico))  # Devuelve True
print(issubclass(CocheElectrico, Vehiculo))  # Devuelve False

Estas funciones son útiles para comprender la jerarquía de clases y para implementar comportamientos específicos basados en el tipo de objeto.

Herencia simple vs. herencia múltiple: diferencias y retos

Herencia es una característica fundamental en la programación orientada a objetos que permite a las clases derivadas heredar atributos y métodos de las clases base. En la herencia simple, una clase hereda de una única clase base, lo que facilita la reutilización del código y mantiene una jerarquía más sencilla y comprensible. Por otro lado, la herencia múltiple permite que una clase herede de varias clases base, ofreciendo mayor flexibilidad pero introduciendo complejidades adicionales y potenciales conflictos.

En Python, la herencia múltiple se implementa al listar múltiples clases base separadas por comas en la definición de una clase:

class A:
    def metodo(self):
        print("Método de A")

class B:
    def metodo(self):
        print("Método de B")

class C(A, B):
    pass

objeto = C()
objeto.metodo()

En este ejemplo, la clase C hereda de A y B. Al llamar a objeto.metodo(), se ejecuta el método de A debido al Orden de Resolución de Métodos (MRO, Method Resolution Order), que determina la precedencia de las clases base. El MRO sigue un orden linealizado que prioriza las clases definidas primero en la lista de herencia.

La herencia múltiple puede ocasionar problemas como el diamante de herencia, donde una clase hereda de dos clases que a su vez heredan de una clase común:

class Vehiculo:
    def descripcion(self):
        print("Vehículo genérico")

class Terrestre(Vehiculo):
    def descripcion(self):
        print("Vehículo terrestre")

class Acuatico(Vehiculo):
    def descripcion(self):
        print("Vehículo acuático")

class Anfibio(Terrestre, Acuatico):
    pass

vehiculo_anfibio = Anfibio()
vehiculo_anfibio.descripcion()

En este caso, Anfibio hereda de Terrestre y Acuatico, que ambos heredan de Vehiculo. Al invocar vehiculo_anfibio.descripcion(), se ejecuta el método de Terrestre según el MRO. Esto puede generar ambigüedad si no se gestiona correctamente, ya que podría no quedar claro qué método se debería ejecutar.

Para manejar estas situaciones, Python utiliza el MRO y se puede emplear la función super() para acceder a métodos de las clases base de forma ordenada:

class Terrestre(Vehiculo):
    def descripcion(self):
        super().descripcion()
        print("Características terrestres")

class Acuatico(Vehiculo):
    def descripcion(self):
        super().descripcion()
        print("Características acuáticas")

class Anfibio(Terrestre, Acuatico):
    def descripcion(self):
        super().descripcion()
        print("Características anfibias")

vehiculo_anfibio = Anfibio()
vehiculo_anfibio.descripcion()

Aquí, cada clase llama al método descripcion de su clase base con super(), lo que permite encadenar las llamadas según el MRO y combinar las descripciones. Sin embargo, esta técnica requiere una comprensión profunda del orden de resolución y puede complicarse en jerarquías extensas.

La herencia simple evita estos problemas al tener una única clase base. Esto simplifica el diseño y hace que el comportamiento de las clases sea más predecible. Muchos lenguajes, como Java, optan por limitar la herencia a una sola clase base y permiten la implementación de múltiples interfaces para alcanzar cierta flexibilidad sin incurrir en los retos de la herencia múltiple.

Los retos de la herencia múltiple incluyen:

  • Ambigüedad en la resolución de métodos y atributos: Cuando las clases base tienen métodos o atributos con el mismo nombre, puede ser difícil determinar cuál utilizar.
  • Complejidad en el mantenimiento: El seguimiento de la jerarquía de clases y el MRO puede hacer que el código sea más difícil de entender y mantener.
  • Posibles conflictos en el diseño: La necesidad de coordinar el comportamiento de múltiples clases base puede llevar a contradicciones y errores si no se planifica cuidadosamente.

Debido a estos desafíos, es común preferir la composición sobre la herencia múltiple. La composición consiste en construir clases complejas mediante la inclusión de objetos de otras clases, delegando responsabilidades en lugar de heredar directamente. Esto promueve un diseño más modular y reduce las dependencias entre clases.

Además, aunque Python permite la herencia múltiple, es recomendable utilizarla con precaución y únicamente cuando aporta un claro beneficio. Es fundamental comprender el MRO y diseñar las clases de manera que minimicen los conflictos y mantengan la coherencia en el comportamiento.

Ejemplos prácticos: diseño de jerarquías de clases

Para ilustrar cómo aplicar la herencia en el diseño de jerarquías de clases, consideremos un ejemplo práctico de un sistema de vehículos. Queremos modelar diferentes tipos de vehículos compartiendo características comunes, pero también con comportamientos específicos.

Comenzamos definiendo una clase base Vehiculo que representará las propiedades y métodos generales de cualquier vehículo.

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

    def arrancar(self):
        print(f"{self.marca} {self.modelo} está arrancando.")

    def detener(self):
        print(f"{self.marca} {self.modelo} se ha detenido.")

La clase Vehiculo tiene atributos como marca y modelo, y métodos comunes arrancar y detener. Ahora, crearemos subclases que heredan de Vehiculo y añaden funcionalidades específicas.

Clase Coche como subclase de Vehiculo:

class Coche(Vehiculo):
    def __init__(self, marca, modelo, num_puertas):
        super().__init__(marca, modelo)
        self.num_puertas = num_puertas

    def abrir_maletero(self):
        print(f"Abriendo el maletero de {self.marca} {self.modelo}.")

La clase Coche añade el atributo num_puertas y el método abrir_maletero, manteniendo los atributos y métodos de Vehiculo gracias a la función super().

Clase Motocicleta como subclase de Vehiculo:

class Motocicleta(Vehiculo):
    def __init__(self, marca, modelo, tipo):
        super().__init__(marca, modelo)
        self.tipo = tipo

    def hacer_caballito(self):
        print(f"{self.marca} {self.modelo} está haciendo un caballito.")

La clase Motocicleta introduce el atributo tipo y el método hacer_caballito, extendiendo la funcionalidad de Vehiculo.

Clase Camion como subclase de Vehiculo:

class Camion(Vehiculo):
    def __init__(self, marca, modelo, capacidad_carga):
        super().__init__(marca, modelo)
        self.capacidad_carga = capacidad_carga

    def cargar(self, peso):
        if peso <= self.capacidad_carga:
            print(f"Cargando {peso} toneladas en {self.marca} {self.modelo}.")
        else:
            print(f"El peso excede la capacidad de carga de {self.capacidad_carga} toneladas.")

La clase Camion incorpora el atributo capacidad_carga y el método cargar, permitiendo manejar operaciones específicas de un camión.

Creación y uso de objetos de las clases:

coche = Coche("Toyota", "Corolla", 4)
moto = Motocicleta("Yamaha", "MT-07", "Deportiva")
camion = Camion("Volvo", "FH16", 25)

coche.arrancar()
coche.abrir_maletero()
coche.detener()
print("---")
moto.arrancar()
moto.hacer_caballito()
moto.detener()
print("---")
camion.arrancar()
camion.cargar(20)
camion.detener()

Resultado esperado:

Toyota Corolla está arrancando.
Abriendo el maletero de Toyota Corolla.
Toyota Corolla se ha detenido.
---
Yamaha MT-07 está arrancando.
Yamaha MT-07 está haciendo un caballito.
Yamaha MT-07 se ha detenido.
---
Volvo FH16 está arrancando.
Cargando 20 toneladas en Volvo FH16.
Volvo FH16 se ha detenido.

Jerarquía de clases con herencia múltiple:

Ahora, imaginemos un vehículo anfibio que puede comportarse como un coche y como una embarcación. Creamos una clase Embarcacion:

class Embarcacion:
    def navegar(self):
        print("La embarcación está navegando.")

    def anclar(self):
        print("La embarcación está anclada.")

La clase Amfibio hereda de Vehiculo y Embarcacion, combinando funcionalidades terrestres y acuáticas:

class Anfibio(Vehiculo, Embarcacion):
    def __init__(self, marca, modelo):
        Vehiculo.__init__(self, marca, modelo)

    def transformar(self, modo):
        if modo == "tierra":
            print(f"{self.marca} {self.modelo} se transforma para moverse en tierra.")
        elif modo == "agua":
            print(f"{self.marca} {self.modelo} se transforma para navegar en agua.")
        else:
            print("Modo desconocido.")

Uso de la clase Anfibio:

anfibio = Anfibio("Gibbs", "Biski")

anfibio.arrancar()
anfibio.transformar("agua")
anfibio.navegar()
anfibio.transformar("tierra")
anfibio.detener()

Resultado esperado:

Gibbs Biski está arrancando.
Gibbs Biski se transforma para navegar en agua.
La embarcación está navegando.
Gibbs Biski se transforma para moverse en tierra.
Gibbs Biski se ha detenido.

En este ejemplo, Anfibio utiliza herencia múltiple para incorporar métodos de Vehiculo y Embarcacion, ofreciendo una funcionalidad híbrida.

Diseño de una jerarquía con clases abstractas:

Para definir comportamientos comunes y obligar a las subclases a implementar ciertos métodos, podemos usar clases abstractas con el módulo abc de Python.

from abc import ABC, abstractmethod

class Animal(ABC):
    @abstractmethod
    def hacer_sonido(self):
        pass

    def dormir(self):
        print("El animal está durmiendo.")

La clase Animal es una clase abstracta con un método abstracto hacer_sonido. Las subclases deben implementar este método.

Subclases que heredan de Animal:

class Perro(Animal):
    def hacer_sonido(self):
        print("El perro ladra: ¡Guau guau!")

class Gato(Animal):
    def hacer_sonido(self):
        print("El gato maúlla: ¡Miau!")

Uso de las clases animales:

animales = [Perro(), Gato()]

for animal in animales:
    animal.hacer_sonido()
    animal.dormir()

Resultado esperado:

El perro ladra: ¡Guau guau!
El animal está durmiendo.
El gato maúlla: ¡Miau!
El animal está durmiendo.

Este diseño asegura que todas las subclases de Animal implementen el método hacer_sonido, garantizando coherencia en la jerarquía.

Polimorfismo y herencia en la jerarquía de clases:

El polimorfismo permite tratar objetos de diferentes clases derivadas como instancias de su clase base, facilitando operaciones sobre colecciones heterogéneas.

Continuando con el ejemplo de figuras geométricas:

class Figura:
    def area(self):
        pass

class Triangulo(Figura):
    def __init__(self, base, altura):
        self.base = base
        self.altura = altura

    def area(self):
        return (self.base * self.altura) / 2

class Circulo(Figura):
    def __init__(self, radio):
        self.radio = radio

    def area(self):
        return math.pi * self.radio ** 2

Cálculo del área de distintas figuras:

figuras = [Triangulo(10, 5), Circulo(7)]

for figura in figuras:
    print(f"El área es: {figura.area():.2f}")

Resultado esperado:

El área es: 25.00
El área es: 153.94

Aquí, utilizamos el polimorfismo para calcular el área de diferentes figuras sin preocuparse por el tipo específico de cada objeto.

Beneficios del uso de herencia en jerarquías de clases:

  • Organización lógica: Agrupa clases relacionadas, reflejando relaciones del mundo real.
  • Mantenibilidad: Facilita actualizaciones y modificaciones al código.
  • Reutilización: Aprovecha código existente, reduciendo redundancias.
  • Extensibilidad: Permite ampliar funcionalidades sin afectar clases base.

Consideraciones al diseñar jerarquías de clases:

  • Simplicidad frente a complejidad: Evitar jerarquías demasiado profundas o anchas que compliquen el mantenimiento.
  • Herencia múltiple con precaución: Puede generar conflictos de nombres y dificultades en la resolución de métodos.
  • Uso adecuado de clases abstractas e interfaces: Define contratos claros para las subclases.

Estos ejemplos prácticos demuestran cómo la herencia es una herramienta esencial para el diseño eficiente y estructurado de sistemas en Python, promoviendo buenas prácticas de programación orientada a objetos.

Para seguir leyendo hazte Plus

¿Ya eres Plus? Accede a la app

Plan mensual

19.00 € /mes

Precio normal mensual: 19 €
47 % DE DESCUENTO

Plan anual

10.00 € /mes

Ahorras 108 € al año
Precio normal anual: 120 €
Aprende Fundamentos GRATIS online

Todas las lecciones de Fundamentos

Accede a todas las lecciones de Fundamentos y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.

Accede GRATIS a Fundamentos y certifícate

En esta lección

Objetivos de aprendizaje de esta lección

  • Comprender el concepto de herencia en programación orientada a objetos.
  • Aprender a implementar herencia en Python, Java y C++.
  • Conocer la diferencia entre herencia simple y múltiple.
  • Dominar la sintaxis para redefinir métodos y usar super().
  • Desarrollar jerarquías de clases coherentes y evitar ambigüedades.
  • Utilizar composición cuando sea más apropiado que la herencia múltiple.
  • Aplicar polimorfismo y buenas prácticas de diseño.