Aprende leyendo en orden

Polimorfismo — Mismo nombre de método, comportamiento distinto por tipo

Aprende el polimorfismo en Python. Sobrescribe métodos del padre en las subclases para que quien llama no piense en tipos, y reemplaza ramas if type(...) con POO limpia — todo con diagramas.

La última vez cubrimos la herencia múltiple y el MRO. Para cerrar la serie de POO, este artículo cubre el tercer pilar — el polimorfismo.

¿Qué es el polimorfismo?

El polimorfismo es la idea de que la misma interfaz (nombre de método) puede hacer cosas distintas según el tipo. Supón que quieres una sola operación «calcular salario», pero quieres que empleados, gerentes e ingenieros usen fórmulas distintas.

Define calculate_salary en la clase padre Employee, y haz que las subclases Manager y Engineer la sobrescriban con su propia fórmula. Ahora el código que llama solo escribe employee.calculate_salary() sin preocuparse de qué clase es cuál, y se ejecuta el cálculo correcto.

Tres clases sobrescriben calculate_salary
Employee(padre)base_salary(sin cambio)= 300kManager(override hijo)base +team x 50k= 1,2MEngineer(override hijo)base +skill x 20k= 380k
El padre Employee define calculate_salary. Las subclases Manager / Engineer lo sobrescriben con sus propias fórmulas. Mismo nombre de método, distintos resultados por clase.

Construir sobre el padre, cambiar la fórmula en cada hijo

Construyamos el ejemplo del salario de verdad. Employee es el padre, con Manager (extra por tamaño de equipo) y Engineer (extra por nivel de habilidad) como hijos. La clave es que los tres definen un método con el mismo nombre calculate_salary.

class Employee:
    def __init__(self, name, base_salary):
        self.name        = name
        self.base_salary = base_salary

    def calculate_salary(self):                  # por defecto = solo salario base
        return self.base_salary


class Manager(Employee):
    def __init__(self, name, base_salary, team_size):
        super().__init__(name, base_salary)
        self.team_size = team_size

    def calculate_salary(self):                  # bonus por tamaño de equipo
        return self.base_salary + self.team_size * 50000


class Engineer(Employee):
    def __init__(self, name, base_salary, skill_level):
        super().__init__(name, base_salary)
        self.skill_level = skill_level

    def calculate_salary(self):                  # bonus por nivel de habilidad
        return self.base_salary + self.skill_level * 20000

Define Employee / Manager / Engineer y confirma que cada calculate_salary retorna un resultado distinto.

① Construye las tres clases siguiendo el ejemplo de arriba (Manager y Engineer deben llamar a super().__init__(name, base_salary) para delegar la init del padre).

② Construye Employee("Ana", 300000), Manager("Carlos", 800000, 8) y Engineer("María", 300000, 4), luego print cada resultado de calculate_salary().

(Cuando se ejecute correctamente, aparecerá la explicación.)

Editor Python

Ejecutar el código para ver el resultado
Cómo se comporta emp.calculate_salary() dentro de un bucle
emp.calculate_salary()(misma línea)emp = ana(Employee)-> 300kemp = carlos(Manager)-> 1,2Memp = maria(Engineer)-> 380kEmployeeManagerEngineer
El cuerpo del bucle es solo emp.calculate_salary() en una sola línea, pero se elige automáticamente un método distinto según la clase de emp — esa es la fuerza del polimorfismo.

Agrupar en una lista — Un bucle al que no le importan los tipos

El polimorfismo realmente muestra su fuerza cuando metes objetos de tipos distintos en una sola lista y los procesas todos juntos. Envuelve el cálculo de salario en una clase PayrollSystem y el cuerpo del bucle se reduce a una sola línea.

class PayrollSystem:
    def __init__(self):
        self.employees = []

    def add(self, employee):
        self.employees.append(employee)

    def total(self):
        result = 0
        for emp in self.employees:                       # los tipos pueden variar
            result += emp.calculate_salary()              # el mismo nombre funciona
        return result


payroll = PayrollSystem()
payroll.add(Employee("Ana", 300000))
payroll.add(Manager("Carlos",  800000, 8))
payroll.add(Engineer("María", 300000, 4))

print(payroll.total())   # 1880000
A PayrollSystem no le importa qué hay dentro
PayrollSystem
  • employees = [...] (tipos mezclados)
  • for emp in self.employees: los recorre
  • Solo llama emp.calculate_salary()
Employee (por defecto)
  • retorna base_salary
Manager
  • base + team * 50k
Engineer
  • base + skill * 20k
Aun con tres clases distintas mezcladas en la lista, PayrollSystem solo llama al mismo nombre de método y se ejecuta el cálculo correcto por tipo.

Reutiliza las tres clases de la Práctica 1 para construir un PayrollSystem que calcule el salario total (la consola guarda el estado, así que las definiciones previas siguen ahí).

① Define class PayrollSystem: con __init__ que inicialice self.employees = []. Implementa add(self, employee) y total(self) (que total recorra un bucle for sumando cada emp.calculate_salary()).

② Construye payroll = PayrollSystem(), luego add tres empleados: Employee("Ana", 300000) / Manager("Carlos", 800000, 8) / Engineer("María", 300000, 4).

③ Recorre payroll.employees imprimiendo emp.name y emp.calculate_salary() línea por línea, luego imprime payroll.total() con el prefijo "total:".

Editor Python

Ejecutar el código para ver el resultado

Cómo se ve sin polimorfismo

Intenta hacer lo mismo sin polimorfismo y acabas escribiendo ramas if type(emp) == ...:. Funciona, pero cada nuevo rol significa otra rama if — y en el momento que olvidas una, tienes un bug.

# (MAL) sin polimorfismo (rama por tipo)
def calc(emp):
    if type(emp) is Manager:
        return emp.base_salary + emp.team_size * 50000
    elif type(emp) is Engineer:
        return emp.base_salary + emp.skill_level * 20000
    else:
        return emp.base_salary


# (BIEN) con polimorfismo (lógica empujada a las clases)
def calc(emp):
    return emp.calculate_salary()         # una línea
Ramas por tipo vs polimorfismo
MAL: ramas por tipoif type ==x N vecescada nuevo tipo= nueva ramaBIEN: polimórficoemp.calculate()nuevo tipo =solo una clasese propagacontenido
El código de ramas por tipo acumula if type en quien llama y crece con cada nuevo tipo. El polimorfismo mantiene a quien llama en una sola línea — añadir un nuevo tipo significa solo añadir una clase.

«A quien llama no le importan los tipos» — ese es el mantra

Una prueba fiable de si tu diseño es polimórfico: ¿el código de quien llama tiene pilas de if type(...) o if isinstance(...)? Si sí, la refactorización estándar es mover ese branching a overrides de métodos en las clases. «Añadir una clase» y «añadir un if» tienden a ser un trade-off.

Duck typing — Solo necesitas el mismo nombre de método

El polimorfismo de Python tiene un sabor más permisivo: el duck typing. El dicho «si camina como un pato y grazna como un pato, es un pato» se convierte en «si una clase tiene el método correcto, no importa qué clase sea».

En el ejemplo de abajo, Cat y Dog no comparten un padre Animal para nada — pero mientras ambos tengan un método speak(), la misma función maneja ambos. Python prioriza «¿tiene el método?» sobre el grafo de herencia.

class Cat:
    def speak(self):
        return "Miau"

class Dog:
    def speak(self):
        return "Guau"

def shout(animal):                # el tipo no se impone
    print(animal.speak())

shout(Cat())   # Miau
shout(Dog())   # Guau

Lenguajes como Java o C# requieren una clase padre compartida para que el polimorfismo funcione. Python está contento mientras el método esté en el momento de la llamada. Eso te da flexibilidad, pero «asegurarse de que los nombres de método signifiquen lo mismo entre clases» se vuelve responsabilidad de quien llama — tenlo en cuenta.

Los dos diseños en una sola diapositiva

Diseño polimórfico vs diseño con ramas por tipo
Diseño polimórfico
  • Código de quien llama — una línea emp.calculate_salary()
  • Añadir un nuevo tipo — solo escribe una nueva clase con calculate_salary
  • Radio de impacto — se queda dentro de las clases
  • Legibilidad — basta con leer «mismo nombre, varía por tipo»
Diseño con ramas por tipo
  • Código de quien llama — pila de if type(emp) is ...
  • Añadir un nuevo tipo — cada rama necesita revisión
  • Radio de impacto — se filtra a quien llama
  • Legibilidad — relees las ramas cada vez
Mismo requisito, dos diseños — pero cuánto código escribe quien llama y hasta dónde se propagan los cambios puede variar enormemente.
QUIZ

Verificación de conocimientos

Responde cada pregunta una a una.

Pregunta 1¿Cuál es la descripción más precisa del polimorfismo?

Pregunta 2El polimorfismo tiende a eliminar qué estructura del código de quien llama?

Pregunta 3¿Cuál es la mejor descripción del duck typing de Python?