Herencia y polimorfismo en Python — super(), override y clases abstractas
La herencia en Python es el mecanismo que permite crear nuevas clases a partir de clases existentes. En vez de escribir el mismo código dos veces, defines lo común en una clase base y lo específico en las subclases. En este artículo vemos herencia, super(), override, polimorfismo, clases abstractas e isinstance, todo lo que entra en FP2.
Tabla de Contenidos
¿Qué es la herencia en Python?
Una subclase hereda todos los atributos y métodos de su superclase y puede añadir los suyos propios o redefinir los heredados.
class Empleado:
def __init__(self, nombre, salario):
self.nombre = nombre
self.salario = salario
def trabajar(self):
return f'{self.nombre} está trabajando'
def __str__(self):
return f'{self.nombre} — {self.salario}€/mes'
class Programador(Empleado): # hereda de Empleado
pass # hereda todo sin añadir nada
p = Programador('Sergio', 2500)
print(p.trabajar()) # → Sergio está trabajando
print(p) # → Sergio — 2500€/mes
Programador hereda __init__, trabajar y __str__ de Empleado sin escribir nada.
Inicialización de subclases — super()
Cuando la subclase necesita su propio __init__, debe llamar al de la superclase usando super():
class Empleado:
def __init__(self, nombre, salario):
self.nombre = nombre
self.salario = salario
class Programador(Empleado):
def __init__(self, nombre, salario, lenguaje):
super().__init__(nombre, salario) # llama al __init__ de Empleado
self.lenguaje = lenguaje # atributo propio de Programador
class Disenador(Empleado):
def __init__(self, nombre, salario, herramienta):
super().__init__(nombre, salario)
self.herramienta = herramienta
class Manager(Empleado):
def __init__(self, nombre, salario, equipo):
super().__init__(nombre, salario)
self.__equipo = equipo
@property
def equipo(self):
return list(self.__equipo)
p = Programador('Sergio', 2500, 'Python')
d = Disenador('María', 2200, 'Figma')
m = Manager('Juan', 3500, ['Sergio', 'María'])
print(p.nombre) # → Sergio (heredado)
print(p.lenguaje) # → Python (propio)
super().__init__(nombre, salario) pasa los parámetros necesarios al __init__ de Empleado. Si no llamas a super().__init__(), los atributos de la superclase no se inicializan, es el error más común con herencia en FP2.
Por qué super() y no el nombre de la clase
# MAL — frágil si cambia la jerarquía
class Programador(Empleado):
def __init__(self, nombre, salario, lenguaje):
Empleado.__init__(self, nombre, salario) # hardcoded
# BIEN — flexible con super()
class Programador(Empleado):
def __init__(self, nombre, salario, lenguaje):
super().__init__(nombre, salario) # siempre correcto
super() siempre apunta a la superclase correcta aunque cambie la jerarquía.
Sustitución de métodos — override
Una subclase puede redefinir (override) cualquier método heredado para dar su propia implementación:
class Empleado:
def __init__(self, nombre, salario):
self.nombre = nombre
self.salario = salario
def trabajar(self):
return f'{self.nombre} está trabajando'
def bonus(self):
return self.salario * 0.10
def __str__(self):
return f'{self.nombre} — {self.salario}€/mes'
class Programador(Empleado):
def __init__(self, nombre, salario, lenguaje):
super().__init__(nombre, salario)
self.lenguaje = lenguaje
def trabajar(self): # override — redefine el método heredado
return f'{self.nombre} está programando en {self.lenguaje}'
def __str__(self): # override de __str__
return f'Programador: {self.nombre} ({self.lenguaje}) — {self.salario}€/mes'
class Manager(Empleado):
def __init__(self, nombre, salario, equipo):
super().__init__(nombre, salario)
self.__equipo = equipo
def trabajar(self):
return f'{self.nombre} está gestionando a {len(self.__equipo)} personas'
def bonus(self): # override — los managers tienen bonus diferente
return self.salario * 0.25
def __str__(self):
return f'Manager: {self.nombre} — {self.salario}€/mes (equipo: {len(self.__equipo)})'
p = Programador('Sergio', 2500, 'Python')
m = Manager('Juan', 3500, ['Sergio', 'María', 'Pedro'])
print(p.trabajar()) # → Sergio está programando en Python
print(m.trabajar()) # → Juan está gestionando a 3 personas
print(p.bonus()) # → 250.0 (heredado de Empleado)
print(m.bonus()) # → 875.0 (redefinido en Manager)
Llamar al método de la superclase desde el override
Puedes extender el comportamiento heredado en vez de sustituirlo completamente:
class Programador(Empleado):
def __str__(self):
base = super().__str__() # llama al __str__ de Empleado
return f'{base} | {self.lenguaje}'
p = Programador('Sergio', 2500, 'Python')
print(p) # → Sergio — 2500€/mes | Python
Polimorfismo — misma interfaz, comportamiento diferente
El polimorfismo permite usar objetos de diferentes clases de forma uniforme — si comparten la misma interfaz (los mismos métodos), puedes tratarlos igual aunque sean tipos distintos:
def mostrar_info(empleado):
print(empleado) # llama a __str__ de cada clase
print(empleado.trabajar()) # llama al trabajar() de cada clase
print(f'Bonus: {empleado.bonus():.2f}€\n')
empleados = [
Programador('Sergio', 2500, 'Python'),
Disenador('María', 2200, 'Figma'),
Manager('Juan', 3500, ['Sergio', 'María'])
]
for emp in empleados:
mostrar_info(emp) # mismo código, comportamiento diferente según el tipo
Programador: Sergio (Python) — 2500€/mes Sergio está programando en Python Bonus: 250.00€ Diseñador: María (Figma) — 2200€/mes María está diseñando con Figma Bonus: 220.00€ Manager: Juan — 3500€/mes (equipo: 2) Juan está gestionando a 2 personas Bonus: 875.00€
La función mostrar_info no sabe ni le importa si recibe un Programador, un Disenador o un Manager, llama a los mismos métodos y cada objeto responde a su manera. Eso es polimorfismo.
isinstance vs type — la diferencia que más confunde
type() devuelve el tipo exacto del objeto. isinstance() comprueba si el objeto es de una clase o de cualquier subclase:
p = Programador('Sergio', 2500, 'Python')
print(type(p)) # → <class '__main__.Programador'>
print(type(p) == Programador) # → True
print(type(p) == Empleado) # → False — p es Programador, no Empleado
print(isinstance(p, Programador)) # → True
print(isinstance(p, Empleado)) # → True — Programador hereda de Empleado
print(isinstance(p, Manager)) # → False
La regla práctica: usa isinstance() casi siempre, respeta la jerarquía de clases. type() solo cuando necesitas comprobar el tipo exacto sin considerar la herencia.
# isinstance en la práctica
def calcular_bonus_total(empleados):
total = 0
for emp in empleados:
if isinstance(emp, Empleado): # True para cualquier subclase
total += emp.bonus()
return total
La clase object — la raíz de todo
En Python 3 todas las clases heredan implícitamente de object:
class Empleado: # equivale a class Empleado(object):
pass
print(isinstance(Empleado('Sergio', 2500), object)) # → True
Esto explica por qué todos los objetos tienen __str__, __repr__ y otros métodos mágicos, los heredan de object.
Atributos de las subclases
Un objeto de una subclase tiene acceso a los atributos públicos de la superclase pero no a los privados:
class Empleado:
def __init__(self, nombre, salario):
self.__nombre = nombre # privado
self.salario = salario # público
class Programador(Empleado):
def __init__(self, nombre, salario, lenguaje):
super().__init__(nombre, salario)
self.lenguaje = lenguaje
def info(self):
print(self.salario) # OK — público
print(self.__nombre) # AttributeError — privado de Empleado
print(self._Empleado__nombre) # funciona pero no recomendado
Para que la subclase acceda a datos de la superclase usa métodos públicos o propiedades, no accedas directamente a los atributos privados con name mangling.
Clases abstractas — contratos que deben cumplirse
Una clase abstracta define métodos que las subclases están obligadas a implementar. No se puede instanciar directamente:
from abc import ABC, abstractmethod
class Empleado(ABC): # hereda de ABC — clase abstracta
def __init__(self, nombre, salario):
self.nombre = nombre
self.salario = salario
@abstractmethod
def trabajar(self): # obligatorio — toda subclase debe implementarlo
pass
@abstractmethod
def bonus(self): # obligatorio
pass
def __str__(self): # no abstracto — las subclases lo heredan
return f'{self.nombre} — {self.salario}€/mes'
class Programador(Empleado):
def __init__(self, nombre, salario, lenguaje):
super().__init__(nombre, salario)
self.lenguaje = lenguaje
def trabajar(self): # obligatorio — implementado
return f'{self.nombre} programa en {self.lenguaje}'
def bonus(self): # obligatorio — implementado
return self.salario * 0.10
# No se puede instanciar la clase abstracta
try:
e = Empleado('Test', 1000)
except TypeError as err:
print(err) # → Can't instantiate abstract class Empleado
# Sí se puede instanciar la subclase concreta
p = Programador('Sergio', 2500, 'Python')
print(p.trabajar()) # → Sergio programa en Python
Si una subclase no implementa todos los métodos abstractos, también es abstracta y no se puede instanciar:
class SubclaseIncompleta(Empleado):
def trabajar(self):
return 'trabajando'
# no implementa bonus() — sigue siendo abstracta
try:
s = SubclaseIncompleta('Test', 1000)
except TypeError as err:
print(err) # → Can't instantiate abstract class SubclaseIncompleta
Clase abstracta vs clase normal — cuándo usar cada una
# USA clase normal cuando:
# - la clase puede instanciarse directamente
# - las subclases son especializaciones opcionales
class Vehiculo:
pass
# USA clase abstracta cuando:
# - la clase es solo un contrato — nunca se instancia directamente
# - todas las subclases DEBEN implementar ciertos métodos
from abc import ABC, abstractmethod
class Figura(ABC):
@abstractmethod
def area(self): pass
@abstractmethod
def perimetro(self): pass
Visualízalo con Python Tutor
Copia este código en pythontutor.com:
class Empleado:
def __init__(self, nombre, salario):
self.nombre = nombre
self.salario = salario
def bonus(self):
return self.salario * 0.10
class Manager(Empleado):
def __init__(self, nombre, salario):
super().__init__(nombre, salario)
def bonus(self):
return self.salario * 0.25
e = Empleado('Ana', 2000)
m = Manager('Juan', 3000)
print(e.bonus())
print(m.bonus())
Observa cómo super().__init__() inicializa los atributos de Empleado en el objeto Manager, y cómo cada objeto llama al bonus() correcto según su clase.
Resumen rápido
# HERENCIA
class SubClase(SuperClase):
def __init__(self, param_propio, *params_super):
super().__init__(*params_super) # SIEMPRE llamar a super().__init__
self.propio = param_propio
# OVERRIDE
class SubClase(SuperClase):
def metodo(self): # redefine el método heredado
base = super().metodo() # opcional — extiende el heredado
return f'{base} + extra'
# POLIMORFISMO
for obj in lista_mixta:
obj.metodo() # llama al correcto según el tipo real
# isinstance vs type
isinstance(obj, Clase) # True para la clase y sus subclases
type(obj) == Clase # True solo para esa clase exacta
# CLASES ABSTRACTAS
from abc import ABC, abstractmethod
class MiAbstracta(ABC):
@abstractmethod
def metodo_obligatorio(self): pass
# No instanciable — solo para heredar
# Subclase que no implementa todos los @abstractmethod → también abstracta
# ERRORES TÍPICOS
# 1. Olvidar super().__init__() → atributos de la superclase no inicializados
# 2. type() en vez de isinstance() → no respeta la jerarquía
# 3. Instanciar clase abstracta → TypeError
# 4. Subclase sin implementar @abstractmethod → TypeError al instanciar
En el próximo artículo practicamos herencia y polimorfismo con programas reales.

2 comentarios