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.
Inheritance and polymorphism in Python — super(), override and abstract classes
What is inheritance?
class Employee:
def __init__(self, name, salary):
self.name = name
self.salary = salary
def work(self):
return f'{self.name} is working'
class Programmer(Employee):
pass # inherits everything
p = Programmer('Sergio', 2500)
print(p.work()) # → Sergio is working
Initialising subclasses — super()
class Employee:
def __init__(self, name, salary):
self.name = name
self.salary = salary
class Programmer(Employee):
def __init__(self, name, salary, language):
super().__init__(name, salary) # calls Employee.__init__
self.language = language
class Designer(Employee):
def __init__(self, name, salary, tool):
super().__init__(name, salary)
self.tool = tool
class Manager(Employee):
def __init__(self, name, salary, team):
super().__init__(name, salary)
self.__team = team
If you don’t call super().__init__(), the superclass attributes won’t be initialised, the most common inheritance mistake in FP2.
Override
class Employee:
def work(self):
return f'{self.name} is working'
def bonus(self):
return self.salary * 0.10
class Programmer(Employee):
def work(self): # override
return f'{self.name} is coding in {self.language}'
class Manager(Employee):
def bonus(self): # override — managers get different bonus
return self.salary * 0.25
Extend instead of replace:
class Programmer(Employee):
def __str__(self):
base = super().__str__() # calls Employee.__str__
return f'{base} | {self.language}'
Polymorphism
def show_info(employee):
print(employee)
print(employee.work())
print(f'Bonus: {employee.bonus():.2f}€\n')
staff = [
Programmer('Sergio', 2500, 'Python'),
Designer('María', 2200, 'Figma'),
Manager('Juan', 3500, ['Sergio', 'María'])
]
for emp in staff:
show_info(emp) # same code, different behaviour per type
show_info doesn’t know or care what type it receives, it calls the same methods and each object responds its own way.
isinstance vs type
p = Programmer('Sergio', 2500, 'Python')
type(p) == Programmer # → True — exact type
type(p) == Employee # → False — p is Programmer, not Employee
isinstance(p, Programmer) # → True
isinstance(p, Employee) # → True — Programmer inherits from Employee
isinstance(p, Manager) # → False
Use isinstance() almost always, respects the class hierarchy. type() only when you need the exact type ignoring inheritance.
Abstract classes
from abc import ABC, abstractmethod
class Employee(ABC):
def __init__(self, name, salary):
self.name = name
self.salary = salary
@abstractmethod
def work(self): pass # subclasses MUST implement this
@abstractmethod
def bonus(self): pass # subclasses MUST implement this
class Programmer(Employee):
def __init__(self, name, salary, language):
super().__init__(name, salary)
self.language = language
def work(self): # mandatory — implemented
return f'{self.name} codes in {self.language}'
def bonus(self): # mandatory — implemented
return self.salary * 0.10
try:
e = Employee('Test', 1000) # cannot instantiate abstract class
except TypeError as err:
print(err)
Abstract vs normal class:
# Normal class — can be instantiated directly
class Vehicle:
pass
# Abstract class — contract only, never instantiated directly
class Shape(ABC):
@abstractmethod
def area(self): pass
@abstractmethod
def perimeter(self): pass
Quick summary
# INHERITANCE
class SubClass(SuperClass):
def __init__(self, own_param, *super_params):
super().__init__(*super_params) # ALWAYS call super().__init__
self.own = own_param
# OVERRIDE
class SubClass(SuperClass):
def method(self):
base = super().method() # optional — extend inherited
return f'{base} + extra'
# POLYMORPHISM
for obj in mixed_list:
obj.method() # calls correct version based on real type
# isinstance vs type
isinstance(obj, Class) # True for class AND subclasses
type(obj) == Class # True only for that exact class
# ABSTRACT CLASSES
from abc import ABC, abstractmethod
class MyAbstract(ABC):
@abstractmethod
def required_method(self): pass
# COMMON MISTAKES
# 1. Forgetting super().__init__() → superclass attributes not initialised
# 2. type() instead of isinstance() → ignores hierarchy
# 3. Instantiating abstract class → TypeError
# 4. Subclass missing @abstractmethod → TypeError when instantiating

2 comentarios