Comportamiento de la Herencia

Beginner

This tutorial is from open-source community. Access the source code

Introducción

En este laboratorio, aprenderás sobre el comportamiento de la herencia en Python. En concreto, te centrarás en cómo funciona la función super() y cómo se implementa la herencia cooperativa. La herencia es un concepto fundamental en la programación orientada a objetos, que permite a las clases heredar atributos y métodos de las clases padre para reutilizar código y crear estructuras de clases jerárquicas.

En esta experiencia práctica, entenderás los diferentes tipos de herencia en Python, incluyendo la herencia simple y múltiple. También aprenderás a usar la función super() para navegar por las jerarquías de herencia, implementar un ejemplo práctico de herencia múltiple cooperativa y aplicar estos conceptos para construir un sistema de validación. El archivo principal creado durante este laboratorio es validate.py.

Este es un Guided Lab, que proporciona instrucciones paso a paso para ayudarte a aprender y practicar. Sigue las instrucciones cuidadosamente para completar cada paso y obtener experiencia práctica. Los datos históricos muestran que este es un laboratorio de nivel intermedio con una tasa de finalización del 69%. Ha recibido una tasa de reseñas positivas del 100% por parte de los estudiantes.

Comprendiendo la Herencia Simple y Múltiple

En este paso, aprenderemos sobre los dos tipos principales de herencia en Python: herencia simple y herencia múltiple. La herencia es un concepto fundamental en la programación orientada a objetos que permite a una clase heredar atributos y métodos de otras clases. También veremos cómo Python determina qué método llamar cuando hay múltiples candidatos, un proceso conocido como resolución de métodos.

Herencia Simple

La herencia simple ocurre cuando las clases forman una sola línea de ascendencia. Puedes pensar en ello como en un árbol genealógico donde cada clase tiene solo un padre directo. Vamos a crear un ejemplo para entender cómo funciona.

Primero, abre una nueva terminal en el WebIDE. Una vez abierta la terminal, inicia el intérprete de Python escribiendo el siguiente comando y luego presionando Enter:

python3

Ahora que estás en el intérprete de Python, crearemos tres clases que formen una cadena de herencia simple. Ingresa el siguiente código:

class A:
    def spam(self):
        print('A.spam')

class B(A):
    def spam(self):
        print('B.spam')
        super().spam()

class C(B):
    def spam(self):
        print('C.spam')
        super().spam()

En este código, la clase B hereda de la clase A, y la clase C hereda de la clase B. La función super() se utiliza para llamar al método de la clase padre.

Después de definir estas clases, podemos averiguar el orden en el que Python busca los métodos. Este orden se llama Orden de Resolución de Métodos (Method Resolution Order, MRO). Para ver el MRO de la clase C, escribe el siguiente código:

C.__mro__

Deberías ver una salida similar a esta:

(<class '__main__.C'>, <class '__main__.B'>, <class '__main__.A'>, <class 'object'>)

Esta salida muestra que Python busca primero un método en la clase C, luego en la clase B, luego en la clase A y, finalmente, en la clase base object.

Ahora, creemos una instancia de la clase C y llamemos a su método spam(). Escribe el siguiente código:

c = C()
c.spam()

Deberías ver la siguiente salida:

C.spam
B.spam
A.spam

Esta salida demuestra cómo funciona super() en una cadena de herencia simple. Cuando C.spam() llama a super().spam(), llama a B.spam(). Luego, cuando B.spam() llama a super().spam(), llama a A.spam().

Herencia Múltiple

La herencia múltiple permite que una clase herede de más de una clase padre. Esto da a una clase acceso a los atributos y métodos de todas sus clases padre. Veamos cómo funciona la resolución de métodos en este caso.

Ingresa el siguiente código en tu intérprete de Python:

class Base:
    def spam(self):
        print('Base.spam')

class X(Base):
    def spam(self):
        print('X.spam')
        super().spam()

class Y(Base):
    def spam(self):
        print('Y.spam')
        super().spam()

class Z(Base):
    def spam(self):
        print('Z.spam')
        super().spam()

Ahora, crearemos una clase M que herede de múltiples clases padre X, Y y Z. Ingresa el siguiente código:

class M(X, Y, Z):
    pass

M.__mro__

Deberías ver la siguiente salida:

(<class '__main__.M'>, <class '__main__.X'>, <class '__main__.Y'>, <class '__main__.Z'>, <class '__main__.Base'>, <class 'object'>)

Esta salida muestra el Orden de Resolución de Métodos para la clase M. Python buscará métodos en este orden.

Creemos una instancia de la clase M y llamemos a su método spam():

m = M()
m.spam()

Deberías ver la siguiente salida:

X.spam
Y.spam
Z.spam
Base.spam

Observa que super() no solo llama al método de la clase padre inmediata. En lugar de eso, sigue el Orden de Resolución de Métodos (MRO) definido por la clase hija.

Creemos otra clase N con las clases padre en un orden diferente:

class N(Z, Y, X):
    pass

N.__mro__

Deberías ver la siguiente salida:

(<class '__main__.N'>, <class '__main__.Z'>, <class '__main__.Y'>, <class '__main__.X'>, <class '__main__.Base'>, <class 'object'>)

Ahora, crea una instancia de la clase N y llama a su método spam():

n = N()
n.spam()

Deberías ver la siguiente salida:

Z.spam
Y.spam
X.spam
Base.spam

Esto muestra un concepto importante: en la herencia múltiple de Python, el orden de las clases padre en la definición de la clase determina el Orden de Resolución de Métodos. La función super() sigue este orden sin importar desde qué clase se llame.

Cuando hayas terminado de explorar estos conceptos, puedes salir del intérprete de Python escribiendo el siguiente código:

exit()

Construyendo un Sistema de Validación con Herencia

En este paso, vamos a construir un sistema de validación práctico utilizando herencia. La herencia es un concepto poderoso en la programación que te permite crear nuevas clases basadas en las existentes. De esta manera, puedes reutilizar código y crear programas más organizados y modulares. Al construir este sistema de validación, verás cómo se puede utilizar la herencia para crear componentes de código reutilizables que se pueden combinar de diferentes maneras.

Creando la Clase Base de Validación

Primero, necesitamos crear una clase base para nuestros validadores. Para hacer esto, crearemos un nuevo archivo en el WebIDE. Así es como puedes hacerlo: haz clic en "Archivo" > "Nuevo Archivo", o puedes usar el atajo de teclado. Una vez que el nuevo archivo esté abierto, llámalo validate.py.

Ahora, agreguemos algo de código a este archivo para crear una clase base Validator. Esta clase servirá como la base para todos nuestros otros validadores.

## validate.py
class Validator:
    @classmethod
    def check(cls, value):
        return value

En este código, hemos definido una clase Validator con un método check. El método check toma un valor como argumento y simplemente lo devuelve sin cambios. El decorador @classmethod se utiliza para hacer de este método un método de clase. Esto significa que podemos llamar a este método en la propia clase, sin tener que crear una instancia de la clase.

Agregando Validadores de Tipo

A continuación, agregaremos algunos validadores que comprueben el tipo de un valor. Estos validadores heredarán de la clase Validator que acabamos de crear. Vuelve al archivo validate.py y agrega el siguiente código:

class Typed(Validator):
    expected_type = object
    @classmethod
    def check(cls, value):
        if not isinstance(value, cls.expected_type):
            raise TypeError(f'Expected {cls.expected_type}')
        return super().check(value)

class Integer(Typed):
    expected_type = int

class Float(Typed):
    expected_type = float

class String(Typed):
    expected_type = str

La clase Typed es una subclase de Validator. Tiene un atributo expected_type, que se establece inicialmente en object. El método check en la clase Typed comprueba si el valor dado es del tipo esperado. Si no lo es, levanta un TypeError. Si el tipo es correcto, llama al método check de la clase padre utilizando super().check(value).

Las clases Integer, Float y String heredan de Typed y especifican el tipo exacto que deben comprobar. Por ejemplo, la clase Integer comprueba si un valor es un entero.

Probando los Validadores de Tipo

Ahora que hemos creado nuestros validadores de tipo, probémoslos. Abre una nueva terminal y inicia el intérprete de Python ejecutando el siguiente comando:

python3

Una vez que el intérprete de Python esté en funcionamiento, podemos importar y probar nuestros validadores. Aquí hay un código para probarlos:

from validate import Integer, String

Integer.check(10)  ## Should return 10

try:
    Integer.check('10')  ## Should raise TypeError
except TypeError as e:
    print(f"Error: {e}")

String.check('10')  ## Should return '10'

Cuando ejecutes este código, deberías ver algo como esto:

10
Error: Expected <class 'int'>
'10'

También podemos usar estos validadores en una función. Intentémoslo:

def add(x, y):
    Integer.check(x)
    Integer.check(y)
    return x + y

add(2, 2)  ## Should return 4

try:
    add('2', '3')  ## Should raise TypeError
except TypeError as e:
    print(f"Error: {e}")

Cuando ejecutes este código, deberías ver:

4
Error: Expected <class 'int'>

Agregando Validadores de Valor

Hasta ahora, hemos creado validadores que comprueban el tipo de un valor. Ahora, agreguemos algunos validadores que comprueben el valor en sí en lugar del tipo. Vuelve al archivo validate.py y agrega el siguiente código:

class Positive(Validator):
    @classmethod
    def check(cls, value):
        if value < 0:
            raise ValueError('Expected >= 0')
        return super().check(value)

class NonEmpty(Validator):
    @classmethod
    def check(cls, value):
        if len(value) == 0:
            raise ValueError('Must be non-empty')
        return super().check(value)

El validador Positive comprueba si un valor es no negativo. Si el valor es menor que 0, levanta un ValueError. El validador NonEmpty comprueba si un valor tiene una longitud distinta de cero. Si la longitud es 0, levanta un ValueError.

Componiendo Validadores con Herencia Múltiple

Ahora, vamos a combinar nuestros validadores utilizando herencia múltiple. La herencia múltiple permite que una clase herede de más de una clase padre. Vuelve al archivo validate.py y agrega el siguiente código:

class PositiveInteger(Integer, Positive):
    pass

class PositiveFloat(Float, Positive):
    pass

class NonEmptyString(String, NonEmpty):
    pass

Estas nuevas clases combinan la comprobación de tipo y la comprobación de valor. Por ejemplo, la clase PositiveInteger comprueba que un valor sea tanto un entero como no negativo. El orden de herencia es importante aquí. Los validadores se comprueban en el orden especificado en la definición de la clase.

Probando los Validadores Compuestos

Probemos nuestros validadores compuestos. En el intérprete de Python, ejecuta el siguiente código:

from validate import PositiveInteger, PositiveFloat, NonEmptyString

PositiveInteger.check(10)  ## Should return 10

try:
    PositiveInteger.check('10')  ## Should raise TypeError
except TypeError as e:
    print(f"Error: {e}")

try:
    PositiveInteger.check(-10)  ## Should raise ValueError
except ValueError as e:
    print(f"Error: {e}")

NonEmptyString.check('hello')  ## Should return 'hello'

try:
    NonEmptyString.check('')  ## Should raise ValueError
except ValueError as e:
    print(f"Error: {e}")

Cuando ejecutes este código, deberías ver:

10
Error: Expected <class 'int'>
Error: Expected >= 0
'hello'
Error: Must be non-empty

Esto muestra cómo podemos combinar validadores para crear reglas de validación más complejas.

Cuando hayas terminado de probar, puedes salir del intérprete de Python ejecutando el siguiente comando:

exit()

Aplicando Validadores a una Clase de Acciones

En este paso, veremos cómo funcionan nuestros validadores en una situación del mundo real. Los validadores son como pequeños verificadores que se aseguran de que los datos que utilizamos cumplan con ciertas reglas. Crearemos una clase Stock. Una clase es como un plano para crear objetos. En este caso, la clase Stock representará una acción en el mercado de valores, y usaremos nuestros validadores para asegurarnos de que los valores de sus atributos (como el número de acciones y el precio) sean válidos.

Creando la Clase de Acciones

Primero, necesitamos crear un nuevo archivo. En el WebIDE, crea un nuevo archivo llamado stock.py. Este archivo contendrá el código para nuestra clase Stock. Ahora, agrega el siguiente código al archivo stock.py:

## stock.py
from validate import PositiveInteger, PositiveFloat

class Stock:
    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

    @property
    def shares(self):
        return self._shares

    @shares.setter
    def shares(self, value):
        self._shares = PositiveInteger.check(value)

    @property
    def price(self):
        return self._price

    @price.setter
    def price(self, value):
        self._price = PositiveFloat.check(value)

    def cost(self):
        return self.shares * self.price

Analicemos lo que hace este código:

  1. Comenzamos importando los validadores PositiveInteger y PositiveFloat de nuestro módulo validate. Estos validadores nos ayudarán a asegurarnos de que el número de acciones sea un entero positivo y el precio sea un número decimal positivo.
  2. Luego definimos una clase Stock. Dentro de la clase, tenemos un método __init__. Este método se llama cuando creamos un nuevo objeto Stock. Toma tres parámetros: name, shares y price, y los asigna a los atributos del objeto.
  3. Usamos propiedades y setters para validar los valores de shares y price. Una propiedad es una forma de controlar el acceso a un atributo, y un setter es un método que se llama cuando intentamos establecer el valor de ese atributo. Cuando establecemos el atributo shares, se llama al método PositiveInteger.check para asegurarnos de que el valor sea un entero positivo. Del mismo modo, cuando establecemos el atributo price, se llama al método PositiveFloat.check para asegurarnos de que el valor sea un número decimal positivo.
  4. Finalmente, tenemos un método cost. Este método calcula el costo total de la acción multiplicando el número de acciones por el precio.

Probando la Clase de Acciones

Ahora que hemos creado nuestra clase Stock, necesitamos probarla para ver si los validadores funcionan correctamente. Abre una nueva terminal y inicia el intérprete de Python. Puedes hacer esto ejecutando el siguiente comando:

python3

Una vez que el intérprete de Python esté en funcionamiento, podemos importar y probar nuestra clase Stock. Ingresa el siguiente código en el intérprete de Python:

from stock import Stock

## Create a valid stock
s = Stock('GOOG', 100, 490.10)
print(f"Name: {s.name}, Shares: {s.shares}, Price: {s.price}")
print(f"Cost: {s.cost()}")

## Try setting an invalid shares value
try:
    s.shares = -10
except ValueError as e:
    print(f"Error setting shares: {e}")

## Try setting an invalid price value
try:
    s.price = "not a price"
except TypeError as e:
    print(f"Error setting price: {e}")

Cuando ejecutes este código, deberías ver una salida similar a la siguiente:

Name: GOOG, Shares: 100, Price: 490.1
Cost: 49010.0
Error setting shares: Expected >= 0
Error setting price: Expected <class 'float'>

Esta salida muestra que nuestros validadores funcionan como se esperaba. La clase Stock no nos permite establecer valores inválidos para shares y price. Cuando intentamos establecer un valor inválido, se genera un error, y podemos capturar e imprimir ese error.

Entendiendo Cómo Ayuda la Herencia

Una de las grandes ventajas de usar nuestros validadores es que podemos combinar fácilmente diferentes reglas de validación. La herencia es un concepto poderoso en Python que nos permite crear nuevas clases basadas en las existentes. Con la herencia múltiple, podemos usar la función super() para llamar a métodos de múltiples clases padre.

Por ejemplo, si queremos asegurarnos de que el nombre de la acción no esté vacío, podemos seguir estos pasos:

  1. Importar el validador NonEmptyString del módulo validate. Este validador nos ayudará a comprobar si el nombre de la acción no es una cadena vacía.
  2. Agregar un setter de propiedad para el atributo name en la clase Stock. Este setter usará el método NonEmptyString.check() para validar el nombre de la acción.

Esto muestra cómo la herencia, especialmente la herencia múltiple con la función super(), nos permite construir componentes flexibles y reutilizables en diferentes combinaciones.

Cuando hayas terminado de probar, puedes salir del intérprete de Python ejecutando el siguiente comando:

exit()

Resumen

En este laboratorio, has aprendido sobre el comportamiento de la herencia en Python y has comprendido varios conceptos clave. Has explorado la diferencia entre herencia simple y múltiple, has entendido cómo la función super() navega por el Orden de Resolución de Métodos (Method Resolution Order, MRO), has aprendido a implementar herencia múltiple cooperativa y has aplicado la herencia para construir un sistema de validación práctico.

También has creado un marco de validación flexible utilizando herencia y lo has aplicado a un ejemplo del mundo real con la clase Stock, lo que muestra cómo la herencia puede crear componentes reutilizables y componibles. Las principales lecciones incluyen cómo funciona super() en la herencia simple y múltiple, la capacidad de la herencia múltiple para componer funcionalidades y el uso de setters de propiedades con validadores. Estos conceptos son fundamentales para la programación orientada a objetos en Python y se utilizan ampliamente en aplicaciones del mundo real.