herencia en Python super override polimorfismo clases abstractas

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.

¿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.

Publicaciones Similares

2 comentarios

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *