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.


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

Publicaciones Similares

2 comentarios

Deja una respuesta

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