Comprender las Características de las Clases en Python

PythonBeginner
Practicar Ahora

Introducción

En este laboratorio, obtendrás una comprensión práctica de los conceptos clave de la programación orientada a objetos (OOP) en Python. Comenzaremos con la encapsulación, aprendiendo a agrupar datos y métodos dentro de una clase y a controlar el acceso a los datos mediante atributos privados.

A continuación, implementarás la herencia para construir relaciones entre clases, lo que promueve la reutilización de código. También exploraremos el polimorfismo, que permite tratar objetos de diferentes clases de manera uniforme. Finalmente, utilizarás el método super() para llamar eficazmente a métodos de una clase padre, y practicarás la herencia múltiple para ver cómo una clase puede heredar de varias clases padre.

Este es un Laboratorio Guiado (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 principiante con una tasa de finalización del 100%. Ha recibido una tasa de revisión positiva del 100% por parte de los estudiantes.

Explorar la Encapsulación con Clases Básicas

En este paso, exploraremos la encapsulación, un principio fundamental de la OOP. La encapsulación implica agrupar datos (atributos) y los métodos que operan sobre esos datos en una sola unidad, una clase. También restringe el acceso directo al estado interno de un objeto, lo que ayuda a prevenir modificaciones accidentales de los datos.

En Python, utilizamos una convención de nomenclatura para indicar que un atributo es "privado". Anteponer un guion bajo simple (ejemplo: _name) a un atributo indica que está destinado a uso interno. Aunque no se aplica estrictamente, es una convención fuerte que los desarrolladores respetan.

Comenzaremos creando dos clases separadas, Dog y Cat, para ver cómo se pueden estructurar.

Primero, localiza el archivo animal_classes.py en el explorador de archivos del lado izquierdo del WebIDE. Ábrelo y añade el siguiente código Python. Este código define una clase Dog y una clase Cat, cada una con un atributo privado _name y métodos para interactuar con él.

## File: animal_classes.py

class Dog:
    def __init__(self, name):
        ## Un prefijo de guion bajo simple indica un atributo "privado".
        self._name = name

    ## Método público para obtener el valor del atributo privado.
    def get_name(self):
        return self._name

    ## Método público para establecer el valor del atributo privado.
    def set_name(self, value):
        self._name = value

    def say(self):
        print(f"{self._name} says: Woof!")

class Cat:
    def __init__(self, name):
        self._name = name

    def get_name(self):
        return self._name

    def set_name(self, value):
        self._name = value

    def say(self):
        print(f"{self._name} says: Meow!")

## Este bloque solo se ejecutará cuando el script se ejecute directamente.
if __name__ == "__main__":
    ## Crear una instancia de la clase Dog
    my_dog = Dog("Buddy")
    print(f"Initial dog name: {my_dog.get_name()}")

    ## Cambiar el nombre del perro usando el método setter
    my_dog.set_name("Rocky")
    print(f"New dog name: {my_dog.get_name()}")
    my_dog.say()

    print("-" * 20)

    ## Crear una instancia de la clase Cat
    my_cat = Cat("Whiskers")
    print(f"Cat name: {my_cat.get_name()}")
    my_cat.say()

Después de añadir el código, guarda el archivo.

Ahora, ejecutemos el script para ver la encapsulación en acción. Abre la terminal en el WebIDE y ejecuta el siguiente comando:

python animal_classes.py

Verás la siguiente salida, que demuestra que estamos interactuando con el atributo privado _name a través de nuestros métodos públicos get_name y set_name.

Initial dog name: Buddy
New dog name: Rocky
Rocky says: Woof!
--------------------
Cat name: Whiskers
Whiskers says: Meow!

Implementar Herencia y Polimorfismo

En el paso anterior, es posible que hayas notado que las clases Dog y Cat comparten mucho código idéntico (__init__, get_name, set_name). Esta es una oportunidad perfecta para usar la herencia. La herencia permite que una nueva clase (la hija o subclase) herede atributos y métodos de una clase existente (la padre o superclase), promoviendo la reutilización de código.

También introduciremos el polimorfismo, que significa "muchas formas". En OOP, se refiere a la capacidad de diferentes clases para responder a la misma llamada de método de sus propias maneras únicas.

Vamos a refactorizar nuestro código. Crearemos una clase padre Animal para albergar el código común y haremos que Dog y Cat hereden de ella. El método say, que es diferente para cada una, demostrará el polimorfismo.

Abre el archivo animal_classes.py y reemplaza todo su contenido con el siguiente código:

## File: animal_classes.py

class Animal:
    def __init__(self, name):
        self._name = name

    def get_name(self):
        return self._name

    def set_name(self, value):
        self._name = value

    def say(self):
        print(f"{self._name} makes a generic animal sound.")

## Dog hereda de Animal
class Dog(Animal):
    ## Esto sobrescribe el método say() de la clase Animal
    def say(self):
        print(f"{self._name} says: Woof!")

## Cat hereda de Animal
class Cat(Animal):
    ## Esto también sobrescribe el método say
    def say(self):
        print(f"{self._name} says: Meow!")

def make_animal_speak(animal_instance):
    animal_instance.say()

if __name__ == "__main__":
    generic_animal = Animal("Creature")
    my_dog = Dog("Buddy")
    my_cat = Cat("Whiskers")

    print("--- Calling say() on each object ---")
    generic_animal.say()
    my_dog.say()
    my_cat.say()

    print("\n--- Demonstrating Polymorphism ---")
    make_animal_speak(generic_animal)
    make_animal_speak(my_dog)
    make_animal_speak(my_cat)

Guarda el archivo. Observa cómo las clases Dog y Cat son ahora mucho más simples. Heredan los métodos __init__, get_name y set_name de Animal. Cada una proporciona su propia versión del método say, lo cual es un ejemplo de sobrescritura de métodos (method overriding).

Ahora, ejecuta el script actualizado desde la terminal:

python animal_classes.py

La salida será:

--- Calling say() on each object ---
Creature makes a generic animal sound.
Buddy says: Woof!
Whiskers says: Meow!

--- Demonstrating Polymorphism ---
Creature makes a generic animal sound.
Buddy says: Woof!
Whiskers says: Meow!

La función make_animal_speak acepta cualquier objeto que tenga un método say. A pesar de que le pasamos diferentes tipos de objetos (Animal, Dog, Cat), funciona correctamente porque cada objeto sabe cómo realizar la acción say a su manera. Este es el poder del polimorfismo.

Utilizar el Método super() para Extender la Funcionalidad

Cuando una clase hija sobrescribe un método de su padre, a veces necesita extender el método del padre, no solo reemplazarlo. La función super() proporciona una forma de llamar al método de la clase padre desde dentro de la clase hija.

Esto es muy común en el método __init__. Una clase hija a menudo necesita realizar sus propios pasos de inicialización además de la inicialización realizada por su padre.

Vamos a añadir atributos únicos a nuestras clases Dog y Cat. El Dog tendrá una age (edad), y el Cat tendrá un color (color). Usaremos super() para asegurar que el método __init__ de la clase padre Animal todavía se llame para establecer el _name.

Modifica el archivo animal_classes.py reemplazando su contenido con el siguiente código:

## File: animal_classes.py

class Animal:
    def __init__(self, name):
        print(f"Animal __init__ called for {name}")
        self._name = name

    def get_name(self):
        return self._name

    def set_name(self, value):
        self._name = value

    def say(self):
        print(f"{self._name} makes a generic animal sound.")

class Dog(Animal):
    def __init__(self, name, age):
        ## Llama al método __init__ del padre para manejar el atributo 'name'
        super().__init__(name)
        print("Dog __init__ called")
        self.age = age

    def say(self):
        ## También podemos usar super() para llamar al método say() del padre
        ## super().say()
        print(f"{self._name} says: Woof! I am {self.age} years old.")

class Cat(Animal):
    def __init__(self, name, color):
        ## Llama al método __init__ del padre
        super().__init__(name)
        print("Cat __init__ called")
        self.color = color

    def say(self):
        print(f"{self._name} says: Meow! I have {self.color} fur.")

if __name__ == "__main__":
    my_dog = Dog("Buddy", 5)
    my_dog.say()

    print("-" * 20)

    my_cat = Cat("Whiskers", "black")
    my_cat.say()

Guarda el archivo. En esta versión, Dog.__init__ y Cat.__init__ primero llaman a super().__init__(name). Esto ejecuta el código en Animal.__init__, que establece el atributo _name. Después de eso, proceden con sus propias inicializaciones específicas (self.age = age y self.color = color).

Ejecuta el script desde la terminal:

python animal_classes.py

La salida demuestra la cadena de llamadas a __init__ y los métodos say extendidos:

Animal __init__ called for Buddy
Dog __init__ called
Buddy says: Woof! I am 5 years old.
--------------------
Animal __init__ called for Whiskers
Cat __init__ called
Whiskers says: Meow! I have black fur.

Practicar la Herencia Múltiple

Python permite que una clase herede de más de una clase padre. Esto se denomina herencia múltiple. Puede ser una herramienta poderosa para combinar funcionalidades de diferentes fuentes, pero también introduce complejidad, particularmente en cómo Python decide qué método del padre usar si tienen el mismo nombre.

Este orden de búsqueda se denomina Orden de Resolución de Métodos (Method Resolution Order - MRO). Python utiliza un algoritmo llamado linealización C3 para determinar un MRO consistente y predecible.

Exploremos esto con un nuevo ejemplo. Abre el archivo multiple_inheritance.py desde el explorador de archivos y añade el siguiente código:

## File: multiple_inheritance.py

class ParentA:
    def speak(self):
        print("Speaking from ParentA")

    def common_method(self):
        print("ParentA's common method")

class ParentB:
    def speak(self):
        print("Speaking from ParentB")

    def common_method(self):
        print("ParentB's common method")

## Child hereda de A, luego de B
class Child_AB(ParentA, ParentB):
    pass

## Child hereda de B, luego de A
class Child_BA(ParentB, ParentA):
    def common_method(self):
        print("Child_BA's own common method")

if __name__ == "__main__":
    child1 = Child_AB()
    child2 = Child_BA()

    print("--- Investigating Child_AB (ParentA, ParentB) ---")
    child1.speak()
    child1.common_method()
    ## El método .mro() muestra el Orden de Resolución de Métodos
    print("MRO for Child_AB:", [c.__name__ for c in Child_AB.mro()])

    print("\n--- Investigating Child_BA (ParentB, ParentA) ---")
    child2.speak()
    child2.common_method()
    print("MRO for Child_BA:", [c.__name__ for c in Child_BA.mro()])

Guarda el archivo. Aquí, Child_AB hereda de ParentA y luego de ParentB. Child_BA hereda en orden inverso. Cuando se llama a un método, Python lo busca en el orden especificado por el MRO.

Ejecuta el script desde la terminal:

python multiple_inheritance.py

Verás la siguiente salida:

--- Investigating Child_AB (ParentA, ParentB) ---
Speaking from ParentA
ParentA's common method
MRO for Child_AB: ['Child_AB', 'ParentA', 'ParentB', 'object']

--- Investigating Child_BA (ParentB, ParentA) ---
Speaking from ParentB
Child_BA's own common method
MRO for Child_BA: ['Child_BA', 'ParentB', 'ParentA', 'object']

A partir de la salida, puedes observar:

  • child1.speak() llama al método de ParentA porque ParentA aparece primero en el MRO de Child_AB.
  • child2.speak() llama al método de ParentB porque ParentB aparece primero en el MRO de Child_BA.
  • child2.common_method() llama a la versión definida directamente en Child_BA, ya que Python la encuentra allí primero antes de verificar a los padres.

Comprender el MRO es crucial para predecir el comportamiento en escenarios de herencia múltiple.

Resumen

En este laboratorio, has adquirido experiencia práctica con cuatro conceptos fundamentales de la programación orientada a objetos en Python.

Comenzaste con la encapsulación, aprendiendo a proteger los datos de la clase por convención utilizando atributos privados y proporcionando métodos públicos para el acceso. Luego, refactorizaste tu código para usar la herencia, creando una clase padre Animal para reducir la duplicación de código en las subclases Dog y Cat.

Mientras implementabas la herencia, viste el polimorfismo en acción, ya que los objetos Dog y Cat respondieron de manera diferente a la misma llamada al método say(). Aprendiste a usar el método super() para llamar y extender la funcionalidad de una clase padre, particularmente dentro del método __init__. Finalmente, exploraste la herencia múltiple y la importancia del Orden de Resolución de Métodos (MRO) para determinar qué método del padre se llama.