SdA A.2.2. Herencia, Subclases y Superclases, Interfaces y Polimorfismo
PYC – 2º de Bachillerato
¡Bienvenidos de nuevo al taller de objetos!
En el A.2.1 aprendiste a construir una clase completa: atributos, constructor, ocultación con __ y sobrecarga de operadores. Pero en el ejercicio del “Sistema de Batallas” ya viste una limitación: si quisieras crear un Guerrero y un Mago, ambos comparten cosas (nombre, nivel, vida) pero cada uno ataca de forma distinta. Copiar y pegar la clase Personaje dos veces, cambiando un poco cada copia, es justo lo que la POO quiere evitar.
Hoy resolvemos eso con tres herramientas: la herencia (reutilizar código entre clases relacionadas), las interfaces (un contrato que varias clases distintas pueden cumplir) y el polimorfismo (tratar objetos diferentes de forma uniforme).
1. Herencia: Superclases y Subclases
La herencia permite crear una clase nueva (subclase) a partir de otra ya existente (superclase), heredando sus atributos y métodos sin volver a escribirlos. La subclase puede además añadir cosas nuevas o sobrescribir (redefinir) el comportamiento heredado.
python
class Personaje:
def __init__(self, nombre: str, nivel: int, vida: float):
self.nombre = nombre
self.nivel = nivel
self.vida = vida
def atacar(self):
print(f"{self.nombre} ataca con sus puños.")
# Subclase: Guerrero HEREDA todo de Personaje
class Guerrero(Personaje):
def __init__(self, nombre: str, nivel: int, vida: float, armadura: int):
super().__init__(nombre, nivel, vida) # reutiliza el constructor del padre
self.armadura = armadura
def atacar(self): # SOBRESCRITURA del método de la superclase
print(f"{self.nombre} ataca con su espada (armadura: {self.armadura}).")
# Subclase: Mago HEREDA todo de Personaje
class Mago(Personaje):
def __init__(self, nombre: str, nivel: int, vida: float, mana: int):
super().__init__(nombre, nivel, vida)
self.mana = mana
def atacar(self):
print(f"{self.nombre} lanza una bola de fuego (maná: {self.mana}).")
guerrero = Guerrero("Thor", 5, 120, armadura=40)
mago = Mago("Loki", 4, 90, mana=60)
guerrero.atacar() # Thor ataca con su espada (armadura: 40).
mago.atacar() # Loki lanza una bola de fuego (maná: 60).
Idea clave:
super().__init__(...)llama al constructor de la superclase para no repetir el código que ya tenía (nombre,nivel,vida). La subclase solo añade lo que le es propio.
2. Interfaces: un contrato compartido entre clases distintas
En lenguajes como Java, una interfaz es un contrato: dice “toda clase que la implemente debe tener estos métodos”, sin importar de qué superclase venga. Python no tiene la palabra clave interface, pero consigue lo mismo con el módulo abc (Abstract Base Classes) y el decorador @abstractmethod.
python
from abc import ABC, abstractmethod
# INTERFAZ: un contrato, no una clase relacionada por herencia "natural"
class Curable(ABC):
@abstractmethod
def curar(self, objetivo):
"""Toda clase que implemente esta interfaz DEBE definir curar()."""
pass
# Clerigo hereda de Personaje Y ADEMÁS implementa la interfaz Curable
class Clerigo(Personaje, Curable):
def __init__(self, nombre, nivel, vida, fe: int):
super().__init__(nombre, nivel, vida)
self.fe = fe
def atacar(self):
print(f"{self.nombre} golpea con su báculo sagrado.")
def curar(self, objetivo): # cumple el contrato de Curable
objetivo.vida += 20
print(f"{self.nombre} cura a {objetivo.nombre}. Vida actual: {objetivo.vida}")
clerigo = Clerigo("Elena", 3, 80, fe=50)
clerigo.curar(guerrero)
Si Clerigo no implementara curar(), Python no dejaría crear el objeto (TypeError, clase abstracta sin completar). Esa es la diferencia entre herencia e interfaz: la herencia comparte código entre clases parecidas; la interfaz obliga a cumplir un contrato aunque las clases no tengan nada más en común.
3. Polimorfismo: la misma llamada, comportamientos distintos
Polimorfismo significa “muchas formas”. En la práctica: puedes tener una lista con objetos de clases distintas (Guerrero, Mago, Clerigo) y llamar al mismo método sobre todos ellos, y cada uno responderá a su manera, sin que tú tengas que preguntar antes de qué clase es cada uno.
python
equipo = [guerrero, mago, clerigo]
for personaje in equipo:
personaje.atacar() # cada objeto ejecuta SU PROPIA versión de atacar()
# Thor ataca con su espada (armadura: 40).
# Loki lanza una bola de fuego (maná: 60).
# Elena golpea con su báculo sagrado.
Idea clave: el bucle no sabe (ni le importa) si
personajees unGuerreroo unMago. Solo sabe que todos sonPersonajey que todos tienenatacar(). Esto es lo que hace que el código sea fácil de ampliar: si mañana añades una claseArquero, este bucle funciona igual, sin tocarlo.
Parte 2: Ejercicio para Moodle (Tarea del Alumnado)
Tarea Práctica: Sistema de Cuerpos Celestes del Sistema Solar
INSTRUCCIONES DE ENTREGA
Debes entregar un archivo sistema_solar_NombreApellido.py junto con una captura de pantalla o PDF de la ejecución por consola del script de prueba.
ACTIVIDAD: Modelado de Cuerpos Celestes (Puntuación: 10 puntos)
Objetivo: Aplicar herencia (superclase/subclases), interfaces (con ABC) y polimorfismo, en un dominio distinto al de los apuntes (astronomía en vez de un videojuego).
Requisitos de la Superclase CuerpoCeleste
- Debe ser una clase abstracta (hereda de
ABC). - Atributos:
nombre(texto) ymasa(float, en kg). - Constructor: recibe
nombreymasa. - Método abstracto
describir(self): cada subclase deberá implementarlo a su manera, mostrando por pantalla un resumen del cuerpo celeste.
Requisitos de la Interfaz Orbitable
- Debe ser una clase abstracta (
ABC) independiente deCuerpoCeleste. - Método abstracto
calcular_periodo_orbital(self, masa_central): debe devolver el periodo orbital en segundos, usando la Tercera Ley de Kepler simplificada:
$$T = 2\pi \sqrt{\dfrac{r^3}{G \cdot M}}$$
donde $r$ es la distancia al cuerpo central (en metros), $G = 6{,}674 \times 10^{-11}$ (constante de gravitación universal) y $M$ es la masa del cuerpo central (en kg, el parámetro masa_central).
Requisitos de las Subclases
Planeta(CuerpoCeleste, Orbitable):
- Añade
distancia_al_sol(en metros) yes_habitable(booleano). - Implementa
describir(): debe mostrar nombre, masa, distancia al Sol y si es habitable. - Implementa
calcular_periodo_orbital()usando la fórmula de Kepler yself.distancia_al_solcomo $r$.
Asteroide(CuerpoCeleste, Orbitable):
- Añade
cinturon(texto, ej. “Cinturón de asteroides”) ydistancia_al_sol. - Implementa
describir()ycalcular_periodo_orbital()de forma análoga al planeta.
Estrella(CuerpoCeleste)(no implementaOrbitable, porque no orbita nada):
- Añade
temperatura_superficial(en Kelvin). - Implementa
describir(): nombre, masa y temperatura superficial.
Script de prueba (al final del archivo)
- Crea un objeto
Estrellapara el Sol (masa ≈ $1{,}989 \times 10^{30}$ kg, temperatura ≈ 5778 K). - Crea un objeto
Planetapara la Tierra (masa ≈ $5{,}972 \times 10^{24}$ kg, distancia al Sol ≈ $1{,}496 \times 10^{11}$ m, habitable = True). - Crea un objeto
Asteroidea tu elección (inventa valores razonables). - Mete los tres objetos en una lista y recórrela con un bucle
for, llamando adescribir()sobre cada uno (esto es el polimorfismo: el mismodescribir(), tres comportamientos distintos). - Para el
Planetay elAsteroide(los que implementanOrbitable), llama acalcular_periodo_orbital()pasando la masa del Sol comomasa_central, y convierte el resultado de segundos a años (dividiendo entre $60 \times 60 \times 24 \times 365$) para comprobar que el periodo de la Tierra sale cercano a 1 año.
CRITERIOS DE EVALUACIÓN (RÚBRICA MOODLE)
| Criterio | Excelente (100%) | Aceptable (60%) | Insuficiente (0-30%) |
|---|---|---|---|
| Herencia (30%) | Planeta, Asteroide y Estrella heredan correctamente de CuerpoCeleste, reutilizando el constructor con super().__init__(). | Herencia implementada pero sin usar super() correctamente, o repite código innecesariamente. | No hay herencia real, o las subclases no funcionan. |
Interfaz Orbitable (30%) | ABC correctamente implementada como clase independiente; Planeta y Asteroide la implementan, Estrella no. | La interfaz existe pero mezclada de forma confusa con la herencia, o falta el método abstracto. | No se usa ABC/@abstractmethod, o la fórmula de Kepler está mal aplicada. |
| Polimorfismo (30%) | El bucle sobre la lista mixta llama a describir() y cada objeto responde con su propia versión, sin usar if/isinstance para distinguir tipos. | Funciona pero usa comprobaciones de tipo (isinstance) en vez de dejar que el polimorfismo actúe solo. | No hay bucle polimórfico, se llama a cada objeto por separado y de forma repetitiva. |
| Script de prueba y cálculo (10%) | Los tres objetos se crean con datos reales razonables y el periodo de la Tierra sale próximo a 1 año. | Faltan algunos datos o el cálculo tiene un error menor de unidades. | No hay script de prueba o los cálculos son incoherentes. |
