Translate

domingo, 23 de marzo de 2025

Programación Orientada a Objetos en Python parte 5


Seguimos con poo y python.

La clase object en Python es la superclase base de todas las clases. Esto significa que cualquier clase que crees hereda, directa o indirectamente, de object. 

object es la raíz de la jerarquía de clases en Python. Si defines una clase sin especificar una superclase, automáticamente hereda de object.


class MiClase:  # Equivalente a class MiClase(object)

    pass


print(issubclass(MiClase, object))  # True


En versiones modernas de Python (desde Python 3), todas las clases heredan implícitamente de object.

Al heredar de object, las clases obtienen algunos métodos fundamentales:


__str__ y __repr__ : Estos métodos definen cómo se representa una instancia como cadena:


class Ejemplo:

    def __str__(self):

        return "Ejemplo como str"


    def __repr__(self):

        return "Ejemplo()"


obj = Ejemplo()

print(str(obj))   # "Ejemplo como str"

print(repr(obj))  # "Ejemplo()"


__eq__ y __hash__ : Permiten comparar objetos y usarlos en estructuras como set o dict.


class Persona:

    def __init__(self, nombre):

        self.nombre = nombre


    def __eq__(self, otro):

        return isinstance(otro, Persona) and self.nombre == otro.nombre


    def __hash__(self):

        return hash(self.nombre)


p1 = Persona("Alice")

p2 = Persona("Alice")

print(p1 == p2)  # True (porque definimos __eq__)

print(hash(p1) == hash(p2))  # True (porque __hash__ usa el nombre)


__class__ : Permite acceder a la clase de un objeto.


print(obj.__class__)  # <class '__main__.Ejemplo'>


Si bien no es necesario, se puede hacer explícita la herencia:


class MiClase(object):  # En Python 3 esto es redundante

    pass


Esto era más importante en Python 2, donde existían diferencias entre clases clásicas y clases de nuevo estilo.

La sobrescritura de métodos (method overriding) ocurre cuando una subclase redefine un método de su superclase con la misma firma (mismo nombre y parámetros). Esto permite modificar o extender el comportamiento heredado.

Cuando una subclase sobrescribe un método de su superclase, la nueva versión reemplaza a la anterior.


class Animal:

    def hacer_sonido(self):

        return "Sonido genérico"


class Perro(Animal):

    def hacer_sonido(self):  # Sobrescribe el método

        return "Guau Guau!"


animal = Animal()

perro = Perro()


print(animal.hacer_sonido())  # Sonido genérico

print(perro.hacer_sonido())   # Guau Guau!


Aquí, Perro redefine hacer_sonido(), cambiando el comportamiento original.

Para reutilizar la implementación de la superclase y agregar nueva funcionalidad, usamos super().


class Vehiculo:

    def describir(self):

        return "Soy un vehículo."


class Coche(Vehiculo):

    def describir(self):

        return super().describir() + " Soy un coche."


c = Coche()

print(c.describir())  # Soy un vehículo. Soy un coche.


super().describir() llama al método original en Vehiculo, evitando reescribir código innecesario.

Podemos sobrescribir el constructor (__init__) de la superclase, llamando a super() para inicializar atributos.


class Persona:

    def __init__(self, nombre):

        self.nombre = nombre


    def presentarse(self):

        return f"Hola, soy {self.nombre}."


class Estudiante(Persona):

    def __init__(self, nombre, universidad):

        super().__init__(nombre)  # Llamamos al __init__ de Persona

        self.universidad = universidad


    def presentarse(self):

        return super().presentarse() + f" Estudio en {self.universidad}."


e = Estudiante("Carlos", "MIT")

print(e.presentarse())  # Hola, soy Carlos. Estudio en MIT.


Aquí, Estudiante extiende __init__ y presentarse(), reutilizando la lógica de Persona.


Podemos redefinir métodos especiales para personalizar el comportamiento de nuestras clases.


class Libro:

    def __init__(self, titulo, autor):

        self.titulo = titulo

        self.autor = autor


    def __str__(self):  # Representación amigable para humanos

        return f"Libro: {self.titulo} de {self.autor}"


    def __repr__(self):  # Representación para depuración

        return f"Libro({repr(self.titulo)}, {repr(self.autor)})"


libro = Libro("1984", "George Orwell")

print(str(libro))   # Libro: 1984 de George Orwell

print(repr(libro))  # Libro('1984', 'George Orwell')


En lenguajes como Java o C++, upcasting y downcasting son técnicas que permiten tratar objetos de una subclase como si fueran de su superclase (upcasting) o convertir un objeto de la superclase en su subclase específica (downcasting).

Python NO tiene upcasting ni downcasting en el sentido estricto porque su sistema de tipos es dinámico y no requiere conversiones explícitas. Sin embargo, se pueden imitar estos conceptos utilizando la herencia y el manejo de instancias.

En upcasting, un objeto de una subclase se trata como si fuera de su superclase. Esto es natural en Python, ya que una subclase hereda automáticamente los métodos y atributos de su superclase.


class Animal:

    def hacer_sonido(self):

        return "Sonido genérico"


class Perro(Animal):

    def hacer_sonido(self):

        return "Guau Guau!"


# Upcasting: Tratamos un Perro como un Animal

animal: Animal = Perro()  # No es necesario hacer conversión

print(animal.hacer_sonido())  # Guau Guau!


En Python, no es necesario realizar una conversión explícita para tratar un objeto de una subclase como su superclase. Simplemente se puede asignar una instancia de la subclase a una variable del tipo de la superclase.

El downcasting ocurre cuando convertimos un objeto de una superclase a su subclase específica. En Python, esto no es seguro y debe hacerse con validaciones, ya que un objeto de la superclase puede no tener los métodos de la subclase.


class Animal:

    def hacer_sonido(self):

        return "Sonido genérico"


class Perro(Animal):

    def hacer_sonido(self):

        return "Guau Guau!"


    def correr(self):

        return "El perro está corriendo."


# Upcasting: Perro como Animal

animal: Animal = Perro()


# Downcasting: Verificamos si el objeto es realmente un Perro

if isinstance(animal, Perro):

    perro: Perro = animal  # No hay conversión explícita, solo reasignación

    print(perro.correr())  # El perro está corriendo.

else:

    print("No se puede hacer downcasting.")


Python no impide el downcasting, pero no lo recomienda porque se basa en la verificación de tipo en tiempo de ejecución (isinstance()). Si intentamos acceder a un método de Perro en un objeto que realmente es Animal, fallará.

En Python, las clases abstractas y los métodos abstractos permiten definir estructuras base que las subclases deben implementar. Se logran con el módulo abc (Abstract Base Class).

Una clase abstracta es aquella que no puede instanciarse directamente y sirve como plantilla para otras clases.

  • Se define usando ABC del módulo abc.  
  • Si una clase tiene al menos un método abstracto, debe ser abstracta.  

Ejemplo:


from abc import ABC, abstractmethod


class Animal(ABC):  # Clase abstracta

    @abstractmethod

    def hacer_sonido(self):

        pass  # No tiene implementación


# Error si intentamos instanciar

# a = Animal()  # TypeError: Can't instantiate abstract class Animal


Animal no se puede instanciar porque tiene métodos abstractos.

Un método abstracto es aquel que debe ser implementado en las subclases.


Ejemplo:


class Perro(Animal):  # Subclase concreta

    def hacer_sonido(self):

        return "Guau Guau!"  # Implementación obligatoria


p = Perro()

print(p.hacer_sonido())  # Guau Guau!


Perro hereda de Animal y debe implementar hacer_sonido(), o Python lanzará un error.

En Python, un método abstracto puede tener una implementación base:


class Ave(ABC):

    @abstractmethod

    def volar(self):

        print("Algunas aves pueden volar")  # Implementación opcional


class Aguila(Ave):

    def volar(self):

        super().volar()

        print("El águila vuela alto")


a = Aguila()

a.volar()


Las subclases pueden llamar super().volar() para reutilizar la implementación base.

También podemos definir propiedades abstractas con @property:


class Figura(ABC):

    @property

    @abstractmethod

    def area(self):

        pass  # Obliga a definir `area` en las subclases


class Cuadrado(Figura):

    def __init__(self, lado):

        self._lado = lado


    @property

    def area(self):

        return self._lado ** 2


c = Cuadrado(4)

print(c.area)  # 16


La subclase debe implementar la propiedad area, o Python lanzará un error.

El polimorfismo es un concepto clave en la Programación Orientada a Objetos (POO) que permite que diferentes clases tengan métodos con el mismo nombre, pero con comportamientos distintos. En Python, el polimorfismo se aplica de manera dinámica y es muy flexible.

Podemos definir métodos con el mismo nombre en diferentes clases y usarlos sin importar el tipo de objeto.


class Perro:

    def hacer_sonido(self):

        return "Guau Guau!"


class Gato:

    def hacer_sonido(self):

        return "Miau Miau!"


# Función polimórfica

def sonido_animal(animal):

    print(animal.hacer_sonido())


# Ambas clases tienen el método `hacer_sonido`, pero con comportamientos diferentes

perro = Perro()

gato = Gato()


sonido_animal(perro)  # Guau Guau!

sonido_animal(gato)   # Miau Miau!


Python permite usar hacer_sonido() sin importar el tipo de objeto porque ambos lo implementan.

Si una clase base define un método, las subclases pueden sobrescribirlo con su propia implementación.


class Ave:

    def volar(self):

        return "Algunas aves pueden volar"


class Aguila(Ave):

    def volar(self):

        return "El águila vuela alto y rápido"


class Pinguino(Ave):

    def volar(self):

        return "El pingüino no puede volar"


# Lista de objetos polimórficos

aves = [Aguila(), Pinguino()]


for ave in aves:

    print(ave.volar())


# Salida:

# El águila vuela alto y rápido

# El pingüino no puede volar


volar() tiene diferentes comportamientos según la subclase.

Podemos forzar a las subclases a implementar métodos específicos usando clases abstractas (ABC).


from abc import ABC, abstractmethod


class Figura(ABC):

    @abstractmethod

    def area(self):

        pass


class Cuadrado(Figura):

    def __init__(self, lado):

        self.lado = lado


    def area(self):

        return self.lado ** 2


class Circulo(Figura):

    def __init__(self, radio):

        self.radio = radio


    def area(self):

        return 3.14 * self.radio ** 2


# Lista polimórfica

figuras = [Cuadrado(4), Circulo(3)]


for figura in figuras:

    print(figura.area())  # Python llama al método `area()` correspondiente


Gracias al polimorfismo, podemos iterar sobre figuras sin preocuparnos del tipo.

Python tambien permite redefinir operadores como +,*,== mediante métodos especiales (dunder methods).


class Punto:

    def __init__(self, x, y):

        self.x = x

        self.y = y


    def __add__(self, otro):

        return Punto(self.x + otro.x, self.y + otro.y)


    def __str__(self):

        return f"({self.x}, {self.y})"


p1 = Punto(2, 3)

p2 = Punto(5, 7)


print(p1 + p2)  # (7, 10)

+ se comporta diferente gracias a la sobrecarga de operadores.