En esta página
Herencia y polimorfismo
Herencia en Python
La herencia permite crear nuevas clases basadas en clases existentes, reutilizando y extendiendo su comportamiento. La clase hija hereda todos los atributos y métodos de la clase padre:
class Animal:
"""Clase base para todos los animales."""
def __init__(self, nombre: str, especie: str) -> None:
self.nombre = nombre
self.especie = especie
self._energia = 100
def comer(self, alimento: str) -> None:
self._energia += 20
print(f"{self.nombre} come {alimento} (energía: {self._energia})")
def dormir(self) -> None:
self._energia += 30
print(f"{self.nombre} duerme plácidamente")
def hacer_sonido(self) -> str:
return "..."
def __repr__(self) -> str:
return f"{type(self).__name__}(nombre={self.nombre!r})"
class Perro(Animal):
"""Subclase de Animal — representa un perro."""
def __init__(self, nombre: str, raza: str) -> None:
super().__init__(nombre, especie="Canis lupus familiaris")
self.raza = raza
self.trucos: list[str] = []
def hacer_sonido(self) -> str:
return "¡Guau!"
def aprender_truco(self, truco: str) -> None:
self.trucos.append(truco)
print(f"{self.nombre} aprendió: {truco}")
def mostrar_trucos(self) -> None:
if self.trucos:
print(f"Trucos de {self.nombre}: {', '.join(self.trucos)}")
else:
print(f"{self.nombre} no sabe ningún truco")
class Gato(Animal):
"""Subclase de Animal — representa un gato."""
def __init__(self, nombre: str, indoor: bool = True) -> None:
super().__init__(nombre, especie="Felis catus")
self.indoor = indoor
def hacer_sonido(self) -> str:
return "¡Miau!"
def ronronear(self) -> None:
print(f"{self.nombre} ronronea... 🐱")
# Usar las subclases
rex = Perro("Rex", "Pastor Alemán")
michi = Gato("Michi")
rex.comer("croquetas") # Método heredado de Animal
rex.aprender_truco("sentarse")
rex.aprender_truco("dar la pata")
rex.mostrar_trucos()
michi.comer("atún")
michi.ronronear()
print(rex.hacer_sonido()) # ¡Guau!
print(michi.hacer_sonido()) # ¡Miau!super() — Acceder a la clase padre
super() es la función que permite llamar a métodos de la clase padre desde una subclase. Es especialmente importante en __init__ y cuando sobreescribes métodos:
class Empleado(Persona):
def __init__(self, nombre: str, año_nac: int, empresa: str, salario: float) -> None:
super().__init__(nombre, año_nac) # Llama a Persona.__init__
self.empresa = empresa
self.salario = salario
def __repr__(self) -> str:
base = super().__repr__() # Llama a Persona.__repr__
return f"{base}, Empleado en {self.empresa}"
class Gerente(Empleado):
def __init__(
self,
nombre: str,
año_nac: int,
empresa: str,
salario: float,
equipo: list[str]
) -> None:
super().__init__(nombre, año_nac, empresa, salario)
self.equipo = equipo
def agregar_al_equipo(self, miembro: str) -> None:
self.equipo.append(miembro)
print(f"{miembro} ahora reporta a {self.nombre}")MRO — Orden de resolución de métodos
Cuando Python busca un método o atributo, sigue el MRO (Method Resolution Order) calculado con el algoritmo C3 linearization:
class A:
def metodo(self) -> str:
return "A"
class B(A):
def metodo(self) -> str:
return f"B → {super().metodo()}"
class C(A):
def metodo(self) -> str:
return f"C → {super().metodo()}"
class D(B, C):
def metodo(self) -> str:
return f"D → {super().metodo()}"
# Ver el MRO
print(D.__mro__)
# (<class 'D'>, <class 'B'>, <class 'C'>, <class 'A'>, <class 'object'>)
d = D()
print(d.metodo()) # D → B → C → AHerencia múltiple y Mixins
Python soporta herencia múltiple. El patrón de Mixin es una forma segura y común de compartir comportamiento sin crear jerarquías complejas:
class JSONMixin:
"""Mixin que agrega serialización JSON a cualquier clase."""
import json as _json
def to_json(self) -> str:
import json
return json.dumps(
{k: v for k, v in self.__dict__.items() if not k.startswith("_")},
default=str,
ensure_ascii=False
)
@classmethod
def from_json(cls, json_str: str) -> "JSONMixin":
import json
datos = json.loads(json_str)
return cls(**datos)
class LogMixin:
"""Mixin que agrega logging básico a métodos."""
def log(self, mensaje: str, nivel: str = "INFO") -> None:
from datetime import datetime
marca = datetime.now().strftime("%H:%M:%S")
print(f"[{marca}][{nivel}][{type(self).__name__}] {mensaje}")
class ValidacionMixin:
"""Mixin para validación de datos."""
def validar(self) -> list[str]:
"""Devuelve lista de errores de validación."""
return []
def es_valido(self) -> bool:
return len(self.validar()) == 0
class UsuarioAPI(JSONMixin, LogMixin, ValidacionMixin):
def __init__(self, nombre: str, email: str) -> None:
self.nombre = nombre
self.email = email
def validar(self) -> list[str]:
errores = []
if not self.nombre:
errores.append("El nombre es requerido")
if "@" not in self.email:
errores.append("El email es inválido")
return errores
def guardar(self) -> None:
if not self.es_valido():
errores = self.validar()
raise ValueError(f"Validación fallida: {errores}")
self.log(f"Guardando usuario {self.nombre}")
# Aquí iría la lógica de persistencia
usuario = UsuarioAPI("Ana García", "[email protected]")
print(usuario.to_json()) # {"nombre": "Ana García", "email": "[email protected]"}
print(usuario.es_valido()) # True
usuario.guardar()ABC — Clases base abstractas
ABC (Abstract Base Class) fuerza a las subclases a implementar ciertos métodos. Si no implementan todos los métodos abstractos, Python lanzará un TypeError al intentar instanciarlas:
from abc import ABC, abstractmethod
import math
class Forma(ABC):
"""Clase base abstracta para formas geométricas."""
@abstractmethod
def area(self) -> float:
"""Calcula el área de la forma."""
...
@abstractmethod
def perimetro(self) -> float:
"""Calcula el perímetro de la forma."""
...
@property
@abstractmethod
def nombre(self) -> str:
"""Nombre de la forma."""
...
# Método concreto que usan todas las subclases
def es_mayor_que(self, otra: "Forma") -> bool:
return self.area() > otra.area()
def __str__(self) -> str:
return f"{self.nombre}: área={self.area():.2f}m², perímetro={self.perimetro():.2f}m"
class Circulo(Forma):
def __init__(self, radio: float) -> None:
if radio <= 0:
raise ValueError("El radio debe ser positivo")
self.radio = radio
@property
def nombre(self) -> str:
return "Círculo"
def area(self) -> float:
return math.pi * self.radio ** 2
def perimetro(self) -> float:
return 2 * math.pi * self.radio
class Rectangulo(Forma):
def __init__(self, ancho: float, alto: float) -> None:
self.ancho = ancho
self.alto = alto
@property
def nombre(self) -> str:
return "Rectángulo"
def area(self) -> float:
return self.ancho * self.alto
def perimetro(self) -> float:
return 2 * (self.ancho + self.alto)
class Triangulo(Forma):
def __init__(self, a: float, b: float, c: float) -> None:
if not (a + b > c and b + c > a and a + c > b):
raise ValueError("Los lados no forman un triángulo válido")
self.a, self.b, self.c = a, b, c
@property
def nombre(self) -> str:
return "Triángulo"
def area(self) -> float:
s = (self.a + self.b + self.c) / 2 # Semiperímetro
return math.sqrt(s * (s-self.a) * (s-self.b) * (s-self.c)) # Heron
def perimetro(self) -> float:
return self.a + self.b + self.c
# No se puede instanciar Forma directamente
# forma = Forma() # TypeError: Can't instantiate abstract class
circulo = Circulo(5)
rectangulo = Rectangulo(4, 6)
triangulo = Triangulo(3, 4, 5)
formas: list[Forma] = [circulo, rectangulo, triangulo]
for forma in formas:
print(forma)
print(f"\nMayor forma: {max(formas, key=lambda f: f.area())}")Protocol — Tipado estructural (duck typing estático)
Los Protocol de typing permiten el tipado estructural: no necesitas heredar de una clase, solo implementar los métodos requeridos:
from typing import Protocol, runtime_checkable
@runtime_checkable
class Serializable(Protocol):
def to_dict(self) -> dict: ...
def to_json(self) -> str: ...
class ConfiguracionApp:
"""No hereda de Serializable, pero implementa sus métodos."""
def __init__(self, host: str, puerto: int) -> None:
self.host = host
self.puerto = puerto
def to_dict(self) -> dict:
return {"host": self.host, "puerto": self.puerto}
def to_json(self) -> str:
import json
return json.dumps(self.to_dict())
def guardar_configuración(obj: Serializable, archivo: str) -> None:
"""Acepta cualquier objeto que implemente Serializable."""
datos = obj.to_json()
print(f"Guardando en {archivo}: {datos}")
config = ConfiguracionApp("localhost", 8080)
guardar_configuración(config, "config.json")
# runtime_checkable permite usar isinstance()
print(isinstance(config, Serializable)) # TruePolimorfismo en la práctica
El polimorfismo permite tratar objetos de distintas clases de la misma manera, siempre que compartan la misma interfaz:
from typing import Sequence
def calcular_area_total(formas: Sequence[Forma]) -> float:
"""Calcula el área total sin importar qué tipo de forma sea."""
return sum(forma.area() for forma in formas)
def describir_coleccion(formas: Sequence[Forma]) -> None:
for i, forma in enumerate(formas, 1):
print(f" {i}. {forma}")
coleccion: list[Forma] = [
Circulo(3),
Rectangulo(5, 8),
Triangulo(6, 8, 10),
Circulo(1),
]
print("=== Formas en la colección ===")
describir_coleccion(coleccion)
print(f"\nÁrea total: {calcular_area_total(coleccion):.2f} m²")Resumen
La herencia permite reutilizar y especializar código. super() asegura que las cadenas de herencia funcionen correctamente. El MRO resuelve el orden de búsqueda en herencia múltiple. Las clases abstractas con ABC garantizan que las subclases implementen los métodos necesarios, mientras que Protocol ofrece tipado estructural más flexible. En la próxima lección aprenderemos a trabajar con archivos y datos JSON.
Inicia sesión para guardar tu progreso