25-Herencia en python




Herencia de Python

🐍 Herencia en Python: Reutilización y Extensión de Clases

La herencia es un mecanismo poderoso en la programación orientada a objetos que permite crear una nueva clase (clase hija o subclase) basada en una clase existente (clase padre o superclase). La clase hija hereda los atributos y métodos de la clase padre, lo que fomenta la reutilización de código y la creación de jerarquías de clases relacionadas.

Conceptos Clave de la Herencia:

  • Clase Padre (Superclase o Clase Base): Es la clase de la que se hereda. Define atributos y métodos comunes que pueden ser compartidos por sus clases hijas.
  • Clase Hija (Subclase o Clase Derivada): Es la nueva clase que hereda de la clase padre. Puede añadir nuevos atributos y métodos, o modificar los heredados.
  • Reutilización de Código: La herencia permite reutilizar el código de la clase padre en las clases hijas, evitando la duplicación y facilitando el mantenimiento.
  • Extensibilidad: Las clases hijas pueden extender la funcionalidad de la clase padre añadiendo nuevos comportamientos o atributos específicos.
  • Jerarquía de Clases: La herencia puede crear jerarquías de clases, donde las clases más generales se encuentran en la parte superior y las clases más específicas en la parte inferior.

Beneficios de la Herencia:

  • Mayor organización del código: Permite estructurar las clases de forma lógica y jerárquica.
  • Reducción de la redundancia: El código común se define una sola vez en la clase padre y se comparte entre las clases hijas.
  • Facilita la extensión y modificación: Se pueden añadir nuevas funcionalidades o modificar las existentes en las clases hijas sin alterar la clase padre (en muchos casos).
  • Promueve el polimorfismo: La herencia es una base para el polimorfismo, donde objetos de diferentes clases pueden responder al mismo método de manera diferente. (Lo veremos en lecciones futuras).

Sintaxis Básica de la Herencia en Python:

Para indicar que una clase hereda de otra en Python, se especifica la clase padre entre paréntesis después del nombre de la clase hija en la definición de la clase.


class ClasePadre:
    # Atributos y métodos de la clase padre
    pass

class ClaseHija(ClasePadre):
    # Atributos y métodos de la clase hija
    # Hereda automáticamente los de ClasePadre
    pass
    

Ejemplo Introductorio: Animal y Perro

Consideremos un ejemplo sencillo donde tenemos una clase padre Animal con una característica común a todos los animales (por ejemplo, hacer un sonido) y una clase hija Perro que hereda de Animal y tiene un sonido específico.

      
class Animal:
    def hacer_sonido(self):
        print("El animal hace un sonido genérico.")

class Perro(Animal):
    def ladrar(self):
        print("¡Guau! ¡Guau!")

mi_animal = Animal()
mi_perro = Perro()

mi_animal.hacer_sonido() # Output: El animal hace un sonido genérico.
mi_perro.hacer_sonido()  # Output: El animal hace un sonido genérico. (Heredado de Animal)
mi_perro.ladrar()       # Output: ¡Guau! ¡Guau! (Propio de Perro)
      
     

En este ejemplo, la clase Perro hereda el método hacer_sonido() de la clase Animal y además define su propio método ladrar().

En los siguientes temas, exploraremos cómo crear clases padre e hija con más detalle, cómo utilizar el método __init__() en la herencia y la función super(), así como cómo añadir propiedades y métodos específicos a las clases hijas.

Crear una clase de padres

👨‍👩‍👧‍👦 Crear una Clase de Padres (Superclase)

La clase de padres es la base de la herencia. Contiene los atributos y métodos que serán comunes a una o más clases hijas. Al definir una clase de padres, nos enfocamos en las características y comportamientos generales que comparten los objetos de las clases más específicas que heredarán de ella.

Sintaxis para Crear una Clase de Padres:

La sintaxis para crear una clase de padres es la misma que para cualquier otra clase en Python:


class NombreDeLaClasePadre:
    def __init__(self, atributo1, atributo2, ...):
        # Inicialización de atributos comunes
        self.atributo1 = atributo1
        self.atributo2 = atributo2
        # ...

    def metodo_comun1(self):
        # Implementación de un método común
        pass

    def metodo_comun2(self, parametro):
        # Implementación de otro método común
        pass
    
  • Definimos la clase utilizando la palabra clave class seguida del nombre de la clase padre (por convención, con la primera letra en mayúscula).
  • Podemos incluir un método __init__() para inicializar los atributos comunes de los objetos de esta clase y de sus clases hijas.
  • Podemos definir métodos que representen comportamientos comunes.

Ejemplo: La Clase de Padres Vehiculo

Consideremos un ejemplo donde queremos modelar diferentes tipos de vehículos. Podemos crear una clase padre llamada Vehiculo que contenga atributos y métodos comunes a todos los vehículos, como una marca, un modelo y la capacidad de moverse.

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

    def moverse(self):
        print("El vehículo se está moviendo.")

    def obtener_informacion(self):
        return f"Marca: {self.marca}, Modelo: {self.modelo}"

# Podemos crear objetos de la clase Vehiculo directamente
mi_vehiculo = Vehiculo("Genérico", "Estándar")
print(mi_vehiculo.obtener_informacion())
mi_vehiculo.moverse()
      
     

En este ejemplo, la clase Vehiculo tiene un constructor que inicializa la marca y el modelo, un método para simular el movimiento y otro para obtener información básica del vehículo.

Próximos Pasos: Crear Clases Hijas

Una vez que tenemos definida nuestra clase de padres Vehiculo, podemos crear clases hijas más específicas, como Coche, Moto o Bicicleta, que heredarán las características de Vehiculo y podrán añadir sus propias particularidades.

Crear una clase de niño

👶 Crear una Clase Hija (Subclase)

Una clase hija hereda atributos y métodos de su clase padre. Podemos definir nuevas características y comportamientos específicos para la clase hija, o incluso modificar los heredados (lo que se conoce como "sobrescritura").

Sintaxis para Crear una Clase Hija:

Para crear una clase hija, se especifica el nombre de la clase padre entre paréntesis después del nombre de la clase hija en la definición de la clase.


class ClaseHija(ClasePadre):
    def __init__(self, atributo_padre1, atributo_padre2, atributo_hijo):
        # Inicialización de atributos
        super().__init__(atributo_padre1, atributo_padre2)
        self.atributo_hijo = atributo_hijo

    def metodo_hijo(self):
        # Implementación de un método específico de la clase hija
        pass

    def metodo_padre_modificado(self):
        # Sobrescribe un método de la clase padre
        pass
    
  • Definimos la clase hija utilizando la palabra clave class seguida de su nombre y el nombre de la clase padre entre paréntesis (ej: class Coche(Vehiculo):).
  • En el método __init__() de la clase hija, es común llamar al método __init__() de la clase padre utilizando la función super() para inicializar los atributos heredados.
  • Podemos añadir nuevos atributos específicos de la clase hija (ej: self.atributo_hijo).
  • Podemos definir nuevos métodos que son propios de la clase hija (ej: metodo_hijo()).
  • Podemos sobrescribir métodos de la clase padre para proporcionar una implementación específica para la clase hija (ej: metodo_padre_modificado()).

Ejemplo: La Clase Hija Coche que Hereda de Vehiculo

Vamos a crear una clase hija llamada Coche que hereda de nuestra clase padre Vehiculo. Un coche tendrá atributos adicionales como el número de ruedas y la capacidad de aparcar.

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

    def moverse(self):
        print("El vehículo se está moviendo.")

    def obtener_informacion(self):
        return f"Marca: {self.marca}, Modelo: {self.modelo}"

class Coche(Vehiculo):
    def __init__(self, marca, modelo, num_ruedas):
        super().__init__(marca, modelo) # Llama al __init__ de la clase padre
        self.num_ruedas = num_ruedas

    def aparcar(self):
        print("El coche está aparcando.")

    # Sobrescribiendo el método moverse de la clase padre
    def moverse(self):
        print("El coche está conduciendo.")

    def obtener_informacion(self):
        info_padre = super().obtener_informacion()
        return f"{info_padre}, Número de ruedas: {self.num_ruedas}"

# Crear un objeto de la clase Coche
mi_coche = Coche("Ford", "Fiesta", 4)
print(mi_coche.obtener_informacion())
mi_coche.moverse()
mi_coche.aparcar()
      
     

En este ejemplo, la clase Coche hereda la marca y el modelo de Vehiculo, tiene su propio atributo num_ruedas y su propio método aparcar(). Además, sobrescribe el método moverse() para proporcionar un comportamiento más específico para un coche y también modifica el método obtener_informacion() utilizando super() para extender la información del padre.

En el siguiente tema, profundizaremos en la función __init__() y cómo se utiliza en la herencia.

Añadir la función "init"()

⚙️ Añadir la Función __init__() en Clases Hijas

El método especial __init__() es el constructor de una clase. Se llama automáticamente cuando se crea un nuevo objeto de esa clase. En el contexto de la herencia, la forma en que manejamos el __init__() en la clase hija es fundamental para asegurar que tanto los atributos de la clase padre como los de la clase hija se inicialicen correctamente.

Cuando la Clase Hija Tiene su Propio __init__():

Si defines un método __init__() en la clase hija, éste sobrescribirá el __init__() de la clase padre. Esto significa que si no llamas explícitamente al __init__() de la clase padre desde el __init__() de la clase hija, los atributos definidos en el constructor del padre no se inicializarán para los objetos de la clase hija.

La Importancia de Llamar a __init__() del Padre:

Para evitar la pérdida de la inicialización de los atributos del padre, es crucial llamar al método __init__() de la clase padre dentro del __init__() de la clase hija. Esto asegura que los atributos heredados se configuren correctamente antes de que se inicialicen los atributos específicos de la clase hija.

Cómo Llamar a __init__() del Padre:

Hay dos formas principales de llamar al método __init__() de la clase padre desde la clase hija:

  1. Usando el nombre de la clase padre directamente:
    
    class ClaseHija(ClasePadre):
        def __init__(self, atributo_padre, atributo_hijo):
            ClasePadre.__init__(self, atributo_padre)
            self.atributo_hijo = atributo_hijo
          
    En este caso, debes pasar explícitamente self como el primer argumento al método __init__() de la clase padre.
  2. Usando la función super():
    
    class ClaseHija(ClasePadre):
        def __init__(self, atributo_padre, atributo_hijo):
            super().__init__(atributo_padre)
            self.atributo_hijo = atributo_hijo
          
    La función super() proporciona una forma más elegante y flexible de acceder a los métodos de la clase padre, especialmente en situaciones de herencia múltiple (que veremos más adelante). No necesitas pasar self explícitamente cuando usas super().

Ejemplo: __init__() en las Clases Vehiculo y Coche

Volvamos a nuestro ejemplo de Vehiculo y Coche para ilustrar el uso de __init__().

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

    def obtener_informacion(self):
        return f"Marca: {self.marca}, Modelo: {self.modelo}"

class Coche(Vehiculo):
    def __init__(self, marca, modelo, num_ruedas):
        super().__init__(marca, modelo) # Llama al __init__ de Vehiculo
        self.num_ruedas = num_ruedas

    def obtener_informacion(self):
        info_padre = super().obtener_informacion()
        return f"{info_padre}, Número de ruedas: {self.num_ruedas}"

mi_coche = Coche("Seat", "León", 4)
print(mi_coche.obtener_informacion())
      
     

En la clase Coche, el método __init__() primero llama a super().__init__(marca, modelo) para inicializar los atributos marca y modelo heredados de Vehiculo, y luego inicializa el atributo específico de Coche, num_ruedas.

Cuándo No Definir __init__() en la Clase Hija:

Si la clase hija no necesita inicializar ningún atributo adicional más allá de los que hereda de la clase padre, puedes omitir la definición de un método __init__() en la clase hija. En este caso, se utilizará automáticamente el __init__() de la clase padre cuando se cree un objeto de la clase hija.

En el siguiente tema, exploraremos con más detalle la función super() y sus ventajas en la herencia.

Utilice la función súper()

🦸‍♂️ Utilice la Función super(): Accediendo a la Clase Padre

La función super() en Python se utiliza para llamar a métodos de la clase padre (o superclase) desde la clase hija (o subclase). Proporciona una forma limpia y flexible de interactuar con la jerarquía de herencia, especialmente en casos de herencia múltiple.

¿Qué hace super()?

Cuando se llama dentro de un método de una clase hija, super() devuelve un objeto proxy que delega las llamadas de métodos a una clase padre. La principal ventaja de usar super() sobre la llamada directa al nombre de la clase padre (ej: ClasePadre.metodo(self, ...)) es su flexibilidad y su capacidad para manejar correctamente la herencia múltiple y el orden de resolución de métodos (MRO - Method Resolution Order).

Sintaxis de super():

La forma más común de usar super() es sin argumentos:


super().metodo_de_la_padre(...)
    

También se puede usar con la clase hija y la instancia como argumentos (ej: super(ClaseHija, self)), pero la forma sin argumentos es generalmente preferida en el código moderno de Python 3.

Ventajas de Usar super():

  • Robustez en herencia múltiple: super() sigue el MRO para determinar qué clase padre se llama, lo que es crucial en situaciones de herencia múltiple complejas.
  • Código más limpio y mantenible: No necesitas referenciar explícitamente el nombre de la clase padre, lo que hace que el código sea más fácil de refactorizar si la jerarquía de clases cambia.
  • Colaboración entre clases en la jerarquía: Permite que los métodos en diferentes niveles de la jerarquía de herencia cooperen y extiendan la funcionalidad sin sobrescribirse completamente.

Ejemplo: Usando super() en __init__() y Otros Métodos

Volvamos a nuestro ejemplo de Vehiculo y Coche para ver cómo super() se utiliza tanto en el constructor como en otros métodos.

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

    def obtener_informacion(self):
        return f"Vehículo - Marca: {self.marca}, Modelo: {self.modelo}"

class Coche(Vehiculo):
    def __init__(self, marca, modelo, num_ruedas):
        print("Inicializando Coche")
        super().__init__(marca, modelo) # Llama al __init__ de Vehiculo
        self.num_ruedas = num_ruedas

    def obtener_informacion(self):
        info_padre = super().obtener_informacion()
        return f"{info_padre}, Número de ruedas: {self.num_ruedas}"

    def moverse(self):
        print("El coche está conduciendo.")

mi_coche = Coche("BMW", "Serie 3", 4)
print(mi_coche.obtener_informacion())
mi_coche.moverse()
      
     

En este ejemplo:

  • En el __init__() de Coche, super().__init__(marca, modelo) llama al constructor de la clase Vehiculo para inicializar la marca y el modelo.
  • En el método obtener_informacion() de Coche, super().obtener_informacion() llama al método obtener_informacion() de la clase Vehiculo, cuyo resultado se utiliza para construir una cadena de información más completa.

Consideraciones al Usar super():

  • super() depende de la correcta definición del MRO de la clase.
  • En la mayoría de los casos de herencia simple, su uso es directo y recomendado.
  • En herencia múltiple, comprender el MRO es crucial para predecir el comportamiento de super().

En resumen, super() es una herramienta poderosa para trabajar con la herencia en Python, permitiendo una colaboración fluida y flexible entre las clases en la jerarquía.

En los siguientes temas, veremos cómo añadir propiedades y métodos específicos a las clases hijas.

Añadir Propiedades

➕ Añadir Propiedades (Atributos) Específicos a las Clases Hijas

Las clases hijas no solo heredan los atributos de sus clases padres, sino que también pueden definir sus propias propiedades únicas. Estas propiedades representan las características específicas de los objetos de la clase hija.

Dónde Definir las Propiedades de la Clase Hija:

Las propiedades específicas de una clase hija generalmente se definen e inicializan dentro de su método __init__(). Después de llamar al __init__() de la clase padre (usando super().__init__(...)), puedes añadir las inicializaciones para los atributos propios de la clase hija.

Ejemplo: Añadiendo Propiedades a la Clase Coche

En nuestro ejemplo de Vehiculo y Coche, la clase Coche tiene una propiedad específica: el número de ruedas (`num_ruedas`).

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

    def obtener_informacion(self):
        return f"Vehículo - Marca: {self.marca}, Modelo: {self.modelo}"

class Coche(Vehiculo):
    def __init__(self, marca, modelo, num_ruedas, tipo_motor):
        super().__init__(marca, modelo) # Inicializa marca y modelo del padre
        self.num_ruedas = num_ruedas   # Propiedad específica de Coche
        self.tipo_motor = tipo_motor   # Otra propiedad específica de Coche

    def obtener_informacion_completa(self):
        info_padre = super().obtener_informacion()
        return f"{info_padre}, Número de ruedas: {self.num_ruedas}, Tipo de motor: {self.tipo_motor}"

mi_coche = Coche("Renault", "Clio", 4, "Gasolina")
print(mi_coche.obtener_informacion_completa())
      
     

En la clase Coche, dentro del método __init__():

  • Primero, llamamos a super().__init__(marca, modelo) para inicializar los atributos heredados de Vehiculo (`marca` y `modelo`).
  • Luego, inicializamos las propiedades específicas de Coche: self.num_ruedas = num_ruedas y self.tipo_motor = tipo_motor.

Además, hemos añadido un método obtener_informacion_completa() que utiliza la información del padre y añade las propiedades específicas del coche.

Otros Métodos para Acceder y Modificar Propiedades:

Al igual que en las clases base, en las clases hijas también podemos definir otros métodos para acceder (`getters`) y modificar (`setters`) las propiedades, si queremos controlar cómo se leen o escriben estos atributos. Sin embargo, para propiedades simples, el acceso directo (ej: mi_coche.num_ruedas) suele ser suficiente.

La Flexibilidad de la Herencia:

La capacidad de añadir propiedades específicas en las clases hijas es una de las ventajas clave de la herencia. Permite crear clases más especializadas que mantienen las características generales de su clase padre pero también incorporan detalles que las hacen únicas.

En el siguiente tema, exploraremos cómo añadir métodos específicos a las clases hijas.

Añadir métodos

✍️ Añadir Métodos Específicos a las Clases Hijas

Además de heredar los métodos de su clase padre, una clase hija puede definir sus propios métodos que son exclusivos de su tipo. Estos métodos implementan comportamientos o funcionalidades específicas de los objetos de la clase hija.

Dónde Definir los Métodos de la Clase Hija:

Los métodos específicos de una clase hija se definen dentro del cuerpo de la definición de la clase, al igual que cualquier otro método. Estos métodos pueden utilizar los atributos heredados de la clase padre y los atributos propios de la clase hija para realizar sus acciones.

Ejemplo: Añadiendo Métodos a la Clase Coche

En nuestro ejemplo de Vehiculo y Coche, podemos añadir métodos específicos para la clase Coche, como abrir_puerta() o encender_motor().

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

    def obtener_informacion(self):
        return f"Vehículo - Marca: {self.marca}, Modelo: {self.modelo}"

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

    def obtener_informacion_completa(self):
        info_padre = super().obtener_informacion()
        return f"{info_padre}, Número de ruedas: {self.num_ruedas}"

    def moverse(self):
        print("El coche está conduciendo.")

    def abrir_puerta(self, numero_puerta):
        print(f"Abriendo la puerta número {numero_puerta} del coche.")

    def encender_motor(self):
        print("El motor del coche se ha encendido.")

mi_coche = Coche("Peugeot", "208", 4)
print(mi_coche.obtener_informacion_completa())
mi_coche.moverse()
mi_coche.abrir_puerta(2)
mi_coche.encender_motor()
      
     

En la clase Coche, hemos añadido dos métodos específicos:

  • abrir_puerta(self, numero_puerta): Permite simular la apertura de una puerta específica del coche.
  • encender_motor(self): Simula el encendido del motor del coche.

Estos métodos son exclusivos de la clase Coche y no están presentes en la clase padre Vehiculo.

Combinación de Métodos Heredados y Propios:

Una de las ventajas de la herencia es que los objetos de la clase hija pueden utilizar tanto los métodos que heredan de la clase padre como los métodos que se definen específicamente en la clase hija, lo que permite crear objetos con un conjunto de funcionalidades ricas y especializadas.

Sobrescritura de Métodos:

Recuerda que una clase hija también puede sobrescribir (redefinir) un método heredado de la clase padre para proporcionar una implementación específica para la clase hija. Vimos un ejemplo de esto con el método moverse() en la clase Coche.

Con esto, hemos cubierto los temas principales de la herencia en Python. ¡Espero que esta lección haya sido clara y útil!




Publicar un comentario

0 Comentarios