Herencia en Python: super(), override y clases abstractas
- Por qué existe la herencia: reutilizar sin duplicar
- Sintaxis básica: la clase hija en Python
- super(): delegar al padre sin hardcodear su nombre
- Sobrescribir métodos: especialización
- Herencia múltiple y el MRO
- Mixins: herencia para comportamientos transversales
- Clases abstractas con ABC: contratos obligatorios
- isinstance(), issubclass() e introspección
- Herencia vs composición: cuándo usar cada una
- Programa completo: sistema de empleados
- Errores clásicos con herencia
- Preguntas frecuentes
La herencia es el mecanismo que permite que una clase nueva aproveche todo el trabajo ya hecho en otra clase, añadiendo o cambiando solo lo que necesita. Sin herencia, una empresa con empleados, directores y técnicos tendría tres clases con los mismos campos nombre, email y calcular_sueldo() copiados y pegados. Con herencia, defines eso una vez y el resto hereda.
Python implementa la herencia de forma pragmática: sin modificadores de visibilidad obligatorios, con herencia múltiple real y con el mecanismo super() que hace que todo funcione correctamente aunque la jerarquía sea compleja.
🏗️ Sintaxis básica: la clase hija en Python
Crear una clase que herede de otra es tan simple como poner el nombre del padre entre paréntesis. La clase hija recibe automáticamente todos los atributos y métodos del padre.
class Animal:
"""Clase base para todos los animales."""
def __init__(self, nombre, especie):
self.nombre = nombre
self.especie = especie
self.vivo = True
def respirar(self):
return f"{self.nombre} respira."
def info(self):
return f"{self.nombre} ({self.especie})"
def __str__(self):
return self.info()
# Herencia: Perro es-un Animal
class Perro(Animal):
pass # hereda TODO — no añade nada aún
rex = Perro("Rex", "Canis lupus")
# Métodos del padre funcionan en la hija:
print(rex.info()) # "Rex (Canis lupus)"
print(rex.respirar()) # "Rex respira."
print(rex.vivo) # True
print(type(rex)) # <class '__main__.Perro'>
Con pass, Perro es simplemente un Animal sin personalizaciones. El valor de la herencia aparece cuando empezamos a añadir comportamiento específico.
🔑 super(): delegar al padre sin hardcodear su nombre
super() devuelve un proxy al padre (o al siguiente en el MRO). Su uso más común es en __init__: la clase hija inicializa sus propios atributos y delega el resto al padre.
class Perro(Animal):
def __init__(self, nombre, raza):
# Llama al __init__ del padre con los argumentos que necesita
super().__init__(nombre, especie="Canis lupus familiaris")
# Después define los atributos propios de Perro
self.raza = raza
def ladrar(self):
return f"{self.nombre}: ¡Guau!"
class Gato(Animal):
def __init__(self, nombre, indoor=True):
super().__init__(nombre, especie="Felis catus")
self.indoor = indoor
def maullar(self):
return f"{self.nombre}: ¡Miau!"
rex = Perro("Rex", "Labrador")
luna = Gato("Luna", indoor=True)
print(rex.info()) # "Rex (Canis lupus familiaris)" ← método del padre
print(rex.raza) # "Labrador" ← atributo propio
print(rex.ladrar()) # "Rex: ¡Guau!"
print(luna.maullar()) # "Luna: ¡Miau!"
print(luna.vivo) # True ← heredado de Animal
¿Por qué super() y no Animal.__init__(self, ...)? Porque la segunda opción hardcodea el nombre del padre. Si cambias la jerarquía o usas herencia múltiple, se rompe. super() siempre hace lo correcto siguiendo el MRO.
object hasta las clases hija. A la derecha, herencia múltiple con el MRO que Python calcula automáticamente — super() sigue ese orden de izquierda a derecha. Infografía: Ciberaula.🔄 Sobrescribir métodos: especialización
Una clase hija puede sobrescribir cualquier método del padre simplemente definiéndolo con el mismo nombre. Python usará la versión de la hija. También puede extenderlo: llamar al padre con super() y añadir comportamiento extra.
class Animal:
def __init__(self, nombre):
self.nombre = nombre
def hablar(self):
return f"{self.nombre} hace un sonido."
def presentar(self):
return f"Soy {self.nombre}, un {type(self).__name__}."
class Perro(Animal):
# SOBRESCRIBE: reemplaza completamente el método del padre
def hablar(self):
return f"{self.nombre}: ¡Guau!"
class Gato(Animal):
def hablar(self):
return f"{self.nombre}: ¡Miau!"
class PerroEntrenado(Perro):
# EXTIENDE: llama al padre y añade más
def hablar(self):
ladrido_base = super().hablar() # llama a Perro.hablar()
return ladrido_base + " (y se sienta)"
# Polimorfismo: la misma interfaz, comportamientos distintos
animales = [Perro("Rex"), Gato("Luna"), PerroEntrenado("Max")]
for a in animales:
print(a.hablar())
# "Rex: ¡Guau!"
# "Luna: ¡Miau!"
# "Max: ¡Guau! (y se sienta)"
🔀 Herencia múltiple y el MRO
Python permite heredar de más de una clase a la vez. Esta es una característica que falta en Java y C#, y que puede ser muy útil pero también confusa si se abusa.
class Volador:
def volar(self):
return f"{self.nombre} vuela."
def aterrizar(self):
return f"{self.nombre} aterriza."
class Nadador:
def nadar(self):
return f"{self.nombre} nada."
def bucear(self):
return f"{self.nombre} bucea."
# Pato hereda de Animal, Volador y Nadador
class Pato(Animal, Volador, Nadador):
def __init__(self, nombre):
super().__init__(nombre) # super() sigue el MRO
def hablar(self):
return f"{self.nombre}: ¡Cuac!"
donald = Pato("Donald")
print(donald.hablar()) # "Donald: ¡Cuac!"
print(donald.volar()) # "Donald vuela." ← de Volador
print(donald.nadar()) # "Donald nada." ← de Nadador
# Ver el MRO:
print(Pato.__mro__)
# (<class 'Pato'>, <class 'Animal'>, <class 'Volador'>,
# <class 'Nadador'>, <class 'object'>)
El MRO define el orden en que Python busca métodos. Si Animal y Volador tuvieran un método con el mismo nombre, Python usaría el de Animal porque aparece antes en el MRO. Puedes ver el MRO con Pato.__mro__ o Pato.mro().
🧩 Mixins: herencia para comportamientos transversales
Un mixin es una clase diseñada específicamente para añadir un comportamiento reutilizable mediante herencia múltiple. No se instancia sola — existe para mezclarse con otras clases.
import json
from datetime import datetime
class JSONMixin:
"""Añade serialización JSON a cualquier clase."""
def to_json(self):
return json.dumps(vars(self), default=str, indent=2)
@classmethod
def from_json(cls, texto):
datos = json.loads(texto)
obj = cls.__new__(cls)
obj.__dict__.update(datos)
return obj
class LogMixin:
"""Añade registro de acciones con timestamp."""
def __init__(self):
self._log = []
def registrar(self, accion):
entrada = f"[{datetime.now().strftime('%H:%M:%S')}] {accion}"
self._log.append(entrada)
def ver_log(self):
return "\n".join(self._log)
# Combinar mixins con la clase real
class Empleado(LogMixin, JSONMixin, Animal):
def __init__(self, nombre, puesto):
Animal.__init__(self, nombre, especie="Homo sapiens")
LogMixin.__init__(self)
self.puesto = puesto
def trabajar(self, tarea):
self.registrar(f"Trabajando en: {tarea}")
return f"{self.nombre} hace: {tarea}"
emp = Empleado("Luis", "Desarrollador")
emp.trabajar("refactorizar módulo")
emp.trabajar("revisar PRs")
print(emp.ver_log()) # log con timestamps
print(emp.to_json()) # serializa el objeto como JSON
🔒 Clases abstractas con ABC: contratos obligatorios
Una clase abstracta define una interfaz que las subclases deben implementar obligatoriamente. Si intentas instanciar una clase abstracta o una subclase que no implementó todos los métodos abstractos, Python lanza un TypeError.
from abc import ABC, abstractmethod
import math
class Forma(ABC):
"""Contrato: toda forma debe poder calcular su área y perímetro."""
@abstractmethod
def area(self):
"""Las subclases DEBEN implementar esto."""
...
@abstractmethod
def perimetro(self):
...
# Método concreto: disponible en todas las subclases
def describir(self):
return (f"{type(self).__name__}: "
f"área={self.area():.2f}, "
f"perímetro={self.perimetro():.2f}")
def __lt__(self, otra):
return self.area() < otra.area()
class Circulo(Forma):
def __init__(self, radio):
self.radio = radio
def area(self):
return math.pi * self.radio ** 2
def perimetro(self):
return 2 * math.pi * self.radio
class Rectangulo(Forma):
def __init__(self, ancho, alto):
self.ancho = ancho
self.alto = alto
def area(self):
return self.ancho * self.alto
def perimetro(self):
return 2 * (self.ancho + self.alto)
# Forma() → TypeError: no se puede instanciar clase abstracta
formas = [Circulo(5), Rectangulo(4, 6)]
for f in formas:
print(f.describir())
menor = min(formas)
print(f"La menor: {menor.describir()}")
🔍 isinstance(), issubclass() e introspección
rex = Perro("Rex", "Labrador")
luna = Gato("Luna")
# isinstance: ¿es este objeto una instancia de esa clase (o de sus padres)?
isinstance(rex, Perro) # True
isinstance(rex, Animal) # True ← herencia funciona
isinstance(rex, Gato) # False
isinstance(rex, (Perro, Gato)) # True ← acepta tupla de clases
# issubclass: relación entre clases (no instancias)
issubclass(Perro, Animal) # True
issubclass(Perro, Perro) # True (una clase es subclase de sí misma)
issubclass(Animal, Perro) # False
# ⚠ issubclass recibe clases, no instancias:
# issubclass(rex, Animal) → TypeError
# type() — comprobación estricta (no herencia):
type(rex) == Perro # True
type(rex) == Animal # False — rex es Perro, no Animal directamente
# Introspección de la jerarquía:
Perro.__bases__ # (Animal,)
Perro.__mro__ # (Perro, Animal, object)
Perro.__name__ # "Perro"
rex.__class__ # <class 'Perro'>
# Atributos del objeto:
vars(rex) # {"nombre":"Rex", "especie":"...", "vivo":True, "raza":"Labrador"}
hasattr(rex, "ladrar") # True
hasattr(rex, "maullar") # False
getattr(rex, "raza", "desconocida") # "Labrador" (con default)
⚖️ Herencia vs composición: cuándo usar cada una
La herencia tiene un problema: crea un acoplamiento fuerte. Si cambias la clase padre, todos los hijos se ven afectados. La composición — donde una clase contiene instancias de otras en lugar de heredar de ellas — es más flexible.
# ──────────────────────────────────────────────
# CON HERENCIA (cuando tiene sentido: "es-un")
# ──────────────────────────────────────────────
class Vehiculo:
def __init__(self, marca, modelo):
self.marca = marca
self.modelo = modelo
def info(self):
return f"{self.marca} {self.modelo}"
class Coche(Vehiculo): # Coche ES-UN Vehiculo ✅
def __init__(self, marca, modelo, puertas):
super().__init__(marca, modelo)
self.puertas = puertas
# ──────────────────────────────────────────────
# CON COMPOSICIÓN (cuando tiene sentido: "tiene-un")
# ──────────────────────────────────────────────
class Motor:
def __init__(self, cilindrada, combustible):
self.cilindrada = cilindrada
self.combustible = combustible
def arrancar(self):
return f"Motor {self.cilindrada}cc arranca."
class CocheCompleto(Vehiculo):
def __init__(self, marca, modelo, cilindrada):
super().__init__(marca, modelo)
self.motor = Motor(cilindrada, "gasolina") # TIENE-UN motor
def arrancar(self):
return self.motor.arrancar() # delega al motor
c = CocheCompleto("Seat", "León", 1500)
print(c.info()) # "Seat León"
print(c.arrancar()) # "Motor 1500cc arranca."
# Ventaja: puedo cambiar el motor sin tocar CocheCompleto:
c.motor = Motor(2000, "diesel")
print(c.arrancar()) # "Motor 2000cc arranca."
🛠️ Programa completo: sistema de empleados
from abc import ABC, abstractmethod
from datetime import date
class Persona(ABC):
"""Clase base abstracta para personas en la empresa."""
def __init__(self, nombre, email, fecha_alta=None):
self.nombre = nombre
self.email = email
self.fecha_alta = fecha_alta or date.today()
self.activo = True
@abstractmethod
def calcular_sueldo(self):
...
@abstractmethod
def rol(self):
...
def antiguedad_anos(self):
delta = date.today() - self.fecha_alta
return delta.days // 365
def resumen(self):
return (f"[{self.rol()}] {self.nombre} "
f"— {self.antiguedad_anos()} año(s) "
f"— Sueldo: {self.calcular_sueldo():.2f} €")
def __str__(self):
return self.resumen()
def __lt__(self, otro):
return self.calcular_sueldo() < otro.calcular_sueldo()
class EmpleadoFijo(Persona):
def __init__(self, nombre, email, sueldo_base, fecha_alta=None):
super().__init__(nombre, email, fecha_alta)
self.sueldo_base = sueldo_base
def rol(self):
return "Fijo"
def calcular_sueldo(self):
bonus = self.sueldo_base * 0.02 * self.antiguedad_anos()
return self.sueldo_base + bonus
class EmpleadoPorHoras(Persona):
def __init__(self, nombre, email, tarifa_hora, horas_mes=160):
super().__init__(nombre, email)
self.tarifa_hora = tarifa_hora
self.horas_mes = horas_mes
def rol(self):
return "Por horas"
def calcular_sueldo(self):
return self.tarifa_hora * self.horas_mes
class Director(EmpleadoFijo):
def __init__(self, nombre, email, sueldo_base, fecha_alta=None):
super().__init__(nombre, email, sueldo_base, fecha_alta)
self._equipo = []
def rol(self):
return "Director"
def calcular_sueldo(self):
sueldo_base = super().calcular_sueldo()
return sueldo_base * 1.20
def incorporar(self, empleado):
self._equipo.append(empleado)
def ver_equipo(self):
if not self._equipo:
return f"{self.nombre} no tiene equipo asignado."
return f"Equipo de {self.nombre}:\n" + "\n".join(
f" · {e}" for e in sorted(self._equipo, reverse=True)
)
# ── Uso del sistema ─────────────────────────────
ana = EmpleadoFijo("Ana García", "ana@empresa.com", 2500, date(2020, 1, 15))
luis = EmpleadoPorHoras("Luis Martín", "luis@empresa.com", 18, 120)
dir1 = Director("María López", "maria@empresa.com", 4000, date(2018, 3, 1))
dir1.incorporar(ana)
dir1.incorporar(luis)
print(ana)
print(luis)
print(dir1)
print()
print(dir1.ver_equipo())
# Ordenar por sueldo:
for e in sorted([ana, luis, dir1]):
print(f" {e.calcular_sueldo():7.2f} € — {e.nombre}")
🐛 Errores clásicos con herencia
1. No llamar a super().__init__() y perder atributos del padre
# ❌
class Perro(Animal):
def __init__(self, nombre, raza):
self.raza = raza # olvidamos llamar a super()
# self.nombre → AttributeError más adelante
# ✅
class Perro(Animal):
def __init__(self, nombre, raza):
super().__init__(nombre, especie="Canis lupus")
self.raza = raza
2. Hardcodear el nombre del padre en lugar de usar super()
# ❌ Frágil: si cambias Animal a Mamifero, se rompe
class Perro(Animal):
def __init__(self, nombre, raza):
Animal.__init__(self, nombre, "Canis lupus") # hardcoded
# ✅ Flexible
class Perro(Animal):
def __init__(self, nombre, raza):
super().__init__(nombre, "Canis lupus")
3. Confundir type() con isinstance() en comprobaciones polimórficas
# ❌ Demasiado estricto: rompe el polimorfismo
def procesar(animal):
if type(animal) == Animal: # Director no pasará aunque herede
print("es animal")
# ✅ Respeta la herencia
def procesar(animal):
if isinstance(animal, Animal):
print("es animal (o subclase)")
4. Heredar solo para reutilizar código sin relación "es-un"
# ❌ Coche no "es-un" Motor
class Coche(Motor):
pass
# ✅ Coche "tiene-un" motor
class Coche:
def __init__(self):
self.motor = Motor(1600, "gasolina")
✅ Resumen y próximos pasos
La herencia permite que las clases hija reutilicen atributos y métodos del padre sin duplicar código. Usa siempre super() — nunca hardcodees el nombre del padre. El MRO define el orden de búsqueda de métodos en herencia múltiple, y super() lo respeta automáticamente. Las clases abstractas con ABC crean contratos que las subclases están obligadas a cumplir. Y si la relación no es conceptualmente «es-un», la composición suele ser una solución más limpia y flexible.
La siguiente lección: polimorfismo en profundidad — duck typing vs herencia para polimorfismo, protocolos y el módulo typing para Type Hints con jerarquías de clases.
❓ Preguntas frecuentes
❓ Preguntas frecuentes sobre Herencia en Python: super(), override y clases abstractas
Las dudas más comunes respondidas de forma clara y directa.
💬 Foro de discusión
¿Tienes dudas sobre Herencia en Python: super(), override y clases abstractas? Comparte tu pregunta con la comunidad.
Todavía no hay mensajes. ¡Sé el primero en participar!