Python logo - Curso de Python desde cero

POO IV: Herencia

Vemos el concepto de Herencia en la Programación Orientada a Objetos

¿Qué tal? Bienvenidos a otra entrega de este curso de programación en python desde cero en el que estamos viendo la programación orientado a objetos. Hemos visto ya conceptos muy importantes relacionados con la POO como el término instancia, el término método, propiedad, encapsulación, etcétera. Vamos a comenzar a abordar ahora un concepto muy importante, el concepto de herencia.

¿Qué es la herencia y la composición?

La herencia y la composición son dos conceptos principales en la programación orientada a objetos que modelan la relación entre dos clases. Impulsan el diseño de una aplicación y determinan cómo debe evolucionar la aplicación a medida que se agregan nuevas funciones o cambian los requisitos.

Ambos permiten la reutilización de código, pero lo hacen de diferentes maneras.

¿Qué es la herencia?

La herencia modela lo que se llama una relación «es-un» (is-a). Esto significa que cuando hay una clase Derivada que hereda de una clase Base, se crea una relación donde Derivada «es-una» versión especializada de Base.

Nota: En una relación de herencia:

  • Las clases que heredan de otra se denominan clases derivadas, subclases o subtipos.
  • Las clases de las que se derivan otras clases se denominan clases base o superclases.
  • Se dice que una clase derivada deriva, hereda o amplía una clase base.

Digamos que tiene una clase base Animal y se deriva de ella para crear una clase Caballo. La relación de herencia establece que a Caballo es un Animal. Esto significa que Caballo hereda la interfaz y la implementación de Animal, y los objetos Caballo se pueden usar para reemplazar objetos Animal en la aplicación.

Esto se conoce como el principio de sustitución de Liskov. El principio establece que “en un programa de computadora, si S es un subtipo de T, entonces los objetos de tipo T pueden ser reemplazados por objetos de tipo S sin alterar ninguna de las propiedades deseadas del programa”.

Verá en este artículo por qué siempre debe seguir el principio de sustitución de Liskov al crear sus jerarquías de clases, y los problemas con los que se encontrará si no lo hace.

¿Qué es la composición?

La composición es un concepto que modela una relación «tiene-un» (has-a). Permite crear tipos complejos combinando objetos de otros tipos. Esto significa que una clase Compuesto (Composite) puede contener un objeto de otra clase Componente (Component). Esta relación significa que una clase Compuesto tiene un objeto de otra clase Componente.

Nota: Las clases que contienen objetos de otras clases generalmente se denominan Compuestos (Composites), mientras que las clases que se utilizan para crear tipos más complejos se denominan Componentes (Components).

Por ejemplo, tu clase Caballo puede estar compuesta por otro objeto de tipo Cola. La composición te permite expresar esa relación diciendo que a Caballo tiene una Cola.

La composición permite reutilizar el código agregando objetos a otros objetos, en lugar de heredar la interfaz y la implementación de otras clases. Tanto la clase Caballo como Perro pueden aprovechar la funcionalidad de Cola a través de la composición sin derivar una clase de la otra.

En este tutorial, aprenderemos sobre la herencia de Python y sus tipos con la ayuda de ejemplos.

Como cualquier otro lenguaje de programación orientada a objetos, Python también admite el concepto de herencia de clases.

La capacidad de herencia nos permite generar una clase adicional tomando como base una clase previamente definida. La clase generada es llamada «subclase» (también conocida como «clase secundaria» o «derivada»), mientras que la clase original a partir de la cual se generó la subclase es llamada «superclase» (también conocida como «clase principal» o «base»).

Sintaxis de herencia de Python

Aquí está la sintaxis de la herencia en Python,

# definición de una superclase
class super_clase:
    # definición de atributos y métodos

# herencia
class sub_clase(super_clase):
    # atributos y métodos de super_clase
    # atributos y métodos de sub_clase

Aquí, heredamos la clase sub_class de la clase super_class.

Ejemplo de herencia en Python

class Animal:

    # atributo y método de la clase padre
    nombre = ""
    
    def comer(self):
        print("Puedo comer")

# hereda de Animal
class Perro(Animal):

    # nuevo método en subclase
    def mostrar(self):
        # accede al atributo nombre de la superclase usando self
        print("Mi nombre es ", self.nombre)

# crea un objeto de la subclase
collie  = Perro()

# accede al atributo y método de la superclase  
collie.nombre = "Cujo"
collie.comer()

Salida:

puedo comer
mi nombre es Cujo

En el ejemplo anterior, hemos derivado una subclase Perro de una superclase Animal. Fíjate en las declaraciones:

collie.nombre = "Cujo"
collie.comer()

Aquí, estamos usando collie (objeto de Perro) para acceder a nombrecomer()de la clase Animal. Esto es posible porque la subclase hereda todos los atributos y métodos de la superclase.

Además, hemos accedido al atributo nombre dentro del método de la clase Perro usando self.

La herencia es una relación «es-un» (is-a)

En Python, la herencia es una relación «es-un» (is-a). Es decir, usamos la herencia solo si existe una relación directa entre dos clases, donde un objeto «es-un» tipo del otro. Por ejemplo:

  1. El coche es un vehículo
  2. La manzana es una fruta
  3. El gato es un animal

Aquí, coche puede heredar de vehiculomanzana puede heredar de Fruta, y así sucesivamente.

Otro ejemplo de Herencia en Python

Echemos un vistazo a otro ejemplo de herencia en Python,

Se puede definir un polígono como una figura geométrica cerrada que consta de al menos tres lados. Para representar un polígono en Python, podemos crear una clase llamada Poligono definida de la siguiente manera:

class Poligono:
    def __init__(self, numero_lados):
        self.n = numero_lados
        self.lados = [0 for i in range(numero_lados)]

    def entrada_lados(self):
        self.lados = [float(input("Introduce lado "+str(i+1)+": ")) for i in range(self.n)]

    def muestra_lados(self):
        for i in range(self.n):
            print("Lado",i+1,"es",self.lados[i])

La clase llamada Poligono tiene dos atributos de datos: n para almacenar el número de lados y lados para almacenar la magnitud de cada lado como una lista. La clase Poligono también tiene dos métodos, entrada_lados() para introducir la magnitud de cada lado y muestra_lados() para mostrar la longitud de los lados.

Como un triángulo es un tipo específico de polígono con tres lados, podemos crear una subclase de Poligono llamada Triangulo . Al heredar de la clase Poligono , la clase Triangulo puede utilizar todos los atributos y métodos definidos en la clase Poligono sin necesidad de redefinirlos (lo que se conoce como reutilización de código). Por lo tanto, la clase Triangulo se puede definir de la siguiente manera:

class Triangulo(Poligono):
    def __init__(self):
        Poligono.__init__(self,3)

    def hallar_area(self):
        a, b, c = self.lados
        # calcula el semiperímetro
        s = (a + b + c) / 2
        area = (s*(s-a)*(s-b)*(s-c)) ** 0.5
        print('El área del triángulo es %0.2f' %area)

Sin embargo, la clase Triangulo tiene un nuevo método hallar_area() para hallar e imprimir el área de nuestro triángulo.

Ahora veamos el código de trabajo completo del ejemplo anterior, incluida la creación de un objeto:

class Poligono:
     # Inicializa el número de lados
    def __init__(self, numero_lados):
        self.n = numero_lados
        self.lados = [0 for i in range(numero_lados)]

    def entrada_lados(self):
        self.lados = [float(input("Introduce lado "+str(i+1)+": ")) for i in range(self.n)]
    
     # método para mostrar la longitud de cada lado del polígono
    def muestra_lados(self):
        for i in range(self.n):
            print("Lado",i+1,"es",self.lados[i])


class Triangulo(Poligono):
     # Inicializamos el número de lados del triángulo a 3 
     # llamando al método __init__ de la clase Poligono
    def __init__(self):
        Poligono.__init__(self,3)

    def hallar_area(self):
        a, b, c = self.lados
         # calcula el semiperímetro
        s = (a + b + c) / 2
         # Usamos la fórmula de Heron para calcular el área del triángulo
        area = (s*(s-a)*(s-b)*(s-c)) ** 0.5
        print('El área del triángulo es %0.2f' %area)

# Crear una instancia de la clase Triangulo
t = Triangulo()

# Crear una instancia de la clase Triangulo
t.entrada_lados()

# Crear una instancia de la clase Triangulo
t.muestra_lados()

# Calcular e imprimir el área del triángulo
t.hallar_area()

Salida:

Introduce lado 1: 3
Introduce lado 2: 8
Introduce lado 3: 6
Lado 1 es 3.0
Lado 2 es 8.0                                  
Lado 3 es 6.0
El área del triángulo es 7.64

En este caso, observamos que aunque no hayamos definido métodos como entrada_lados() o muestra_lados() de forma independiente para la clase Triangulo, todavía pudimos utilizarlos.

Cuando un atributo no se encuentra en la clase actual, la búsqueda continúa en la clase base. Este proceso se repite recursivamente si la clase base también se deriva de otras clases

Sobreescritura de métodos en la herencia de Python

En el ejemplo anterior, vemos que el objeto de la subclase puede acceder al método de la superclase.

Sin embargo, ¿qué ocurre si el mismo método está presente tanto en la superclase como en la subclase?

En este caso, el método de la subclase anula el método de la superclase. Este concepto se conoce en Python como Sobreescritura de métodos.

Ejemplo de Sobreescritura de métodos en la herencia de Python

class Animal:

    # atributo y método de la clase padre
    nombre = ""
    
    def comer(self):
        print("Puedo comer")

# hereda de Animal
class Perro(Animal):

    # sobre escribe el método eat()
    def comer(self):
        print("Me gusta comer huesos")

# crear un objeto de la subclase
collie = Perro()

# llama al método comer() en el objeto collie
collie.comer()

Salida:

me gusta comer huesos

En el ejemplo anterior, el mismo método comer() está presente tanto en la clase Perro como en la clase Animal.

Ahora, cuando llamamos al método comer() usando el objeto de la subclase Perro, el método llama al de la clase Perro.

Esto se debe a que el método comer() de la subclase Perro sobreescribe el mismo método de la superclase Animal.

El método super() en la herencia de Python

Previamente vimos que el mismo método en la subclase sobreescribe el método en la superclase.

Sin embargo, si necesitamos acceder al método de la superclase desde la subclase, usamos el método super(). Por ejemplo:

class Animal:

    # atributo y método de la clase padre
    nombre = ""
    
    def comer(self):
        print("Puedo comer")

# hereda de Animal
class Perro(Animal):

    # sobre escribe el método eat()
    def comer(self):
        
         # llama al método comer() de la superclase usando super()
        super().comer()
        print("Me gusta comer huesos")

# crear un objeto de la subclase
collie = Perro()

# llama al método comer() en el objeto collie
collie.comer()

Salida:

puedo comer 
me gusta comer huesos

En el ejemplo anterior, el método comer() de la subclase Perro sobreescribe el mismo método de la superclase Animal.

Dentro de clase Perro hemos usado:

super().comer()

Llama al método comer() de la superclase Animal desde la subclase Perro.

Entonces, cuando llamamos al método comer() usando el objeto collie

collie.comer()

Se ejecuta tanto la versión sobreescrita como la versión de la superclase del método comer().

El método isinstance() en la herencia de Python

El método isinstance() nos permite comprobar si un objeto (primer argumento) es una subclase o instancia de una clase (segundo argumento.

class Animal:

    nombre = ""
    
    def comer(self):
        print("Puedo comer")

class Perro(Animal):

    def comer(self):    
        print("Me gusta comer huesos")

collie = Perro()

resultado = isinstance(collie, Perro)
resultado_2 = isinstance(collie, Animal)

print(resultado)
print(resultado_2)

Salida:

True
True

Usos de la herencia

  1. Dado que una clase hija puede heredar todas las funcionalidades de la clase padre, esto permite la reutilización del código.
  2. Una vez desarrollada una funcionalidad, basta con heredarla. No hay necesidad de reinventar la rueda. Esto permite un código más limpio y más fácil de mantener.
  3. Dado que también puedes agregar sus propias funcionalidades en la clase secundaria, puede heredar solo las funcionalidades útiles y definir otras características requeridas.

Herencia múltiple de Python

En este tutorial, aprenderemos sobre la herencia múltiple en Python con la ayuda de ejemplos.

Una clase puede derivarse de más de una superclase en Python. Esto se llama herencia múltiple .

Por ejemplo, una clase Murcielago se deriva de las superclases MamiferoAnimalAlado. Tiene sentido porque el murciélago es un mamífero además de un animal alado.

Sintaxis de herencia múltiple de Python

class SuperClase1:
    # características de la SuperClase1

class SuperClase2:
    # características de la SuperClase2

class MultiDerivada(SuperClase1, SuperClase2):
    # características de SuperClase1 + SuperClase2 + clase Multiderivada

Aquí, la MultiDerivada se deriva de las clases SuperClase1SuperClase2.

Ejemplo de herencia múltiple de Python

class Mamifero:
    def mamifero_info(self):
        print("Los mamíferos pueden dar a luz directamente.")

class AnimalAlado:
    def animal_alado_info(self):
        print("Los animales alados pueden aletear.")

class Murcielago(Mamifero, AnimalAlado):
    pass

# crear un objeto de la clase Murcielago
bat1 = Murcielago()

bat1.mamifero_info()
bat1.animal_alado_info()

Salida:

Los mamíferos pueden dar a luz directamente.
Los animales alados pueden aletear.

En el ejemplo anterior, la clase Murcielago se deriva de dos superclases: Mamifero y AnimalAlado. Fíjate en las declaraciones:

bat1 = Murcielago()
bat1.mamifero_info()
bat1.animal_alado_info()

Aquí, estamos usando bat1 (objeto de Murcielago) para acceder a los métodos mamifero_info()animal_alado_info() de las clases Mamifero y AnimalAlado respectivamente.

Herencia multinivel de Python

En Python, no solo podemos derivar una clase de la superclase, sino que también se puede derivar una clase de la clase derivada. Esta forma de herencia se conoce como herencia multinivel.

Aquí está la sintaxis de la herencia multinivel:

class SuperClase:
    # Código de la superclase aquí

class ClaseDerivada1(SuperClase):
    # Código de la clase derivada 1

class ClaseDerivada2(ClaseDerivada1):
    # Código de la clase derivada 2

Aquí la clase ClaseDerivada1 se deriva de la clase SuperClase, y la clase ClaseDerivada2 se deriva de la clase ClaseDerivada1.

Ejemplo de herencia multinivel en Python

class SuperClase:

    def super_metodo(self):
        print("Llamada al método de la SuperClase")

 # define la clase que deriva de SuperClase
class ClaseDerivada_1(SuperClase):

    def metodo_derivado_1(self):
        print("Llamada al método de la clase derivada 1")

 # definir cle lase que deriva de ClaseDerivada_1
class ClaseDerivada_2(ClaseDerivada_1):

    def metodo_derivado_2(self):
        print("Llamada al método de la clase derivada 2")

 # crear un objeto de ClaseDerivada_2
objt = ClaseDerivada_2()

objt.super_metodo()  # Salida: "Llamada al método de la SuperClase"

objt.metodo_derivado_1()  # Salida: "Llamada al método de la clase derivada 1"

objt.metodo_derivado_2()  # Salida: "Llamada al método de la clase derivada 2"

Salida:

Llamada al método de la SuperClase
Llamada al método de la clase derivada 1
Llamada al método de la clase derivada 2

En el ejemplo anterior, ClaseDerivada_2 se deriva de ClaseDerivada_1, que se deriva de SuperClase.

Esto significa que ClaseDerivada_2 hereda todos los atributos y métodos de ambos, ClaseDerivada_1 y SuperClase.

Por lo tanto, estamos usando objt (objeto de ClaseDerivada_2) para llamar a métodos de la SuperClase, ClaseDerivada_1 y ClaseDerivada_2.

Orden de resolución de métodos (MRO) en Python

Si dos superclases tienen el mismo nombre de método y la clase derivada llama a ese método, Python usa el Orden de resolución de métodos, conocido como MRO, para buscar el método correcto a llamar. Por ejemplo:

class SuperClase_1:
    def info(self):
        print("Llamada al método de la SuperClase 1")

class SuperClase_2:
    def info(self):
        print("Llamada al método de la SuperClase 2")

class ClaseDerivada(SuperClase_1, SuperClase_2):
    pass

objt = ClaseDerivada()
objt.info()  

# Salida: "Llamada al método de la SuperClase 1"

Aquí, SuperClase_1 y SuperClase_2 ambas clases definen un método info().

Así que cuando info() se llama usando el objeto objt de la clase ClaseDerivada, Python usa el MRO para determinar a qué método llamar.

En este caso, el MRO especifica que los métodos deben heredarse primero de la superclase más a la izquierda, de modo que info() de SuperClase1 se llama antes que al de la de SuperClase2.

Scroll al inicio