Herencia en Python: simple, sobrescritura y múltiple

1. Descripción general de la herencia en Python

En Python, la herencia es un mecanismo por el cual una clase hija hereda métodos y atributos de una clase padre. Esto mejora la reutilización del código y agiliza el mantenimiento. Es uno de los conceptos clave de la programación orientada a objetos (OOP) y resulta especialmente útil en el desarrollo de sistemas a gran escala y en proyectos de larga duración.

Funciones básicas de la herencia

  • Reutilización de código: Una vez escrita una clase, sus funcionalidades pueden utilizarse también en otras clases, por lo que se puede evitar el código duplicado.
  • Facilidad de mantenimiento: Como los cambios en la clase padre se reflejan automáticamente en las clases hijas, las correcciones y las ampliaciones de funcionalidad pueden realizarse de forma eficiente.
class ParentClass:
    def greet(self):
        print("Hola, soy la clase padre.")

class ChildClass(ParentClass):
    def greet(self):
        print("Hola, soy la clase hija.")
En este ejemplo, ChildClass sobrescribe un método de ParentClass. Dado que el método greet está sobrescrito, en la clase hija se muestra un saludo propio.

2. Herencia simple en Python

Herencia simple es una forma en la que una clase hija hereda funcionalidades de una única clase padre. Esta es la forma básica de herencia en Python, que permite mantener la simplicidad del código y, al mismo tiempo, ofrecer extensibilidad.

Sintaxis básica y ejemplo de herencia simple

class Car:
    def __init__(self, brand, color):
        self.brand = brand
        self.color = color

    def describe(self):
        print(f"Este auto es un {self.brand} de color {self.color}.")

class ElectricCar(Car):
    def __init__(self, brand, color, battery_size):
        super().__init__(brand, color)
        self.battery_size = battery_size

    def describe_battery(self):
        print(f"La capacidad de la batería es de {self.battery_size} kWh.")
En este ejemplo, la clase ElectricCar hereda las funcionalidades de la clase Car y, además, añade la función de describir la capacidad de la batería. Se utiliza super() para llamar al constructor de la clase padre e inicializar los atributos comunes (marca y color).

3. Sobrescritura de métodos

Sobrescritura es la capacidad de que una subclase vuelva a definir un método de su superclase. Con ello, es posible aprovechar los métodos de la superclase y, al mismo tiempo, modificar el comportamiento en la subclase.

Ejemplo de sobrescritura

class Animal:
    def speak(self):
        print("Sonido de animal")

class Dog(Animal):
    def speak(self):
        print("¡Guau guau!")
En este ejemplo, la clase Dog sobrescribe el método speak de la clase Animal. De este modo, en una instancia de la clase Dog se imprime «guau guau», mientras que en una instancia de la clase Animal se muestra «sonido de animal».

4. Herencia múltiple

Herencia múltiple permite que una subclase herede de varias superclases. Con ello, es posible integrar las funcionalidades de diferentes clases en una sola clase, pero hay que tener cuidado, ya que la implementación puede volverse compleja.

Ejemplo y consideraciones sobre la herencia múltiple

class A:
    def greet(self):
        print("Saludo de A")

class B:
    def greet(self):
        print("Saludo de B")

class C(A, B):
    pass

c = C()
c.greet()  # Se muestra "Saludo de A" (según el MRO, se prioriza la primera clase)
En Python, el MRO (Method Resolution Order) determina qué método de las superclases se invoca. Para comprobar este orden, utilice C.mro(). La herencia múltiple es potente, pero debe usarse teniendo en cuenta los posibles conflictos entre superclases y el orden de los métodos.

5. Ejemplos prácticos con herencia

La herencia resulta útil en muchos escenarios incluso en la programación cotidiana. Por ejemplo, en un sistema de gestión de empleados de una empresa, heredar desde una clase básica de empleado para crear clases con cargos específicos permite la reutilización y la ampliación del código.

Ejemplo práctico de un sistema de gestión de empleados

class Employee:
    def __init__(self, first, last, pay):
        self.first = first
        self.last = last
        self.pay = pay

    def fullname(self):
        return f'{self.first} {self.last}'

class Manager(Employee):
    def __init__(self, first, last, pay, employees=None):
        super().__init__(first, last, pay)
        self.employees = employees if employees is not None else []

    def add_employee(self, employee):
        if employee not in self.employees:
            self.employees.append(employee)

    def print_employees(self):
        for emp in self.employees:
            print(emp.fullname())
En este ejemplo, la clase Manager hereda de la clase Employee y añade funcionalidades de gestión de empleados. Al mantener las funcionalidades comunes de la clase base, se amplían las funciones según el cargo específico.

6. Mejores prácticas de herencia y contraste con la composición

La herencia es muy poderosa, pero el uso excesivo puede complicar el código. En particular, la herencia múltiple puede volver complejas las relaciones entre clases, por lo que debe usarse con cautela. En estos casos, se recomienda utilizar composición en lugar de herencia.

Ejemplo de composición

La composición es un patrón de diseño en el que una clase tiene otra clase como componente (instancia) y delega funcionalidad.
class Engine:
    def start(self):
        print("El motor ha arrancado.")

class Car:
    def __init__(self, engine):
        self.engine = engine

    def start(self):
        self.engine.start()

engine = Engine()
car = Car(engine)
car.start()  # Se mostrará "El motor ha arrancado."
Así, la composición es una forma de compartir funcionalidad entre clases sin usar herencia. Al incluir solo la funcionalidad necesaria, el código se vuelve más flexible y fácil de mantener.

7. Resumen

La herencia en Python es una herramienta poderosa para mejorar la reutilización de código y la extensibilidad. Al comprender técnicas como la herencia simple, la herencia múltiple y la sobrescritura, se pueden crear programas eficientes y fáciles de mantener. Por otro lado, es importante considerar cuándo optar por la composición y diseñar adecuadamente. Al dominar el uso adecuado de la herencia, se puede construir una base de código flexible y robusta.