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.