Ir al contenido
wannacode

Polimorfismo

En resumen: El polimorfismo es cuando la misma llamada da un resultado distinto según el objeto, porque cada clase hija sobrescribe el método de la clase padre.

Problema

El pago con un if para cada tipo

La empresa tiene empleados normales, gerentes con bono y becarios a media jornada. Alguien hizo una sola clase Employee con un campo «rol», y el pago se calcula así:

def monthly_pay(e):
if e.role == "gerente":
return e.salary + e.bonus
elif e.role == "becario":
return e.salary // 2
return e.salary

Funciona. Pero entonces llega un nuevo tipo de empleado: un contratista. Necesitas un nuevo elif aquí, y en la función de los impuestos, y en la función de la nómina, y en todos los lugares donde alguien preguntó por el rol. Y cada empleado lleva un campo de bono, aunque solo los gerentes reciben bono.

El código del gerente está repartido por todo el programa en lugar de estar en el gerente.

La idea

Cada clase decide por sí misma

Con la herencia de la lección anterior, Manager e Intern son clases separadas. Deja que cada una diga cuál es su pago mensual. La clase padre tiene una versión general, y la hija escribe su propia versión del mismo método:

class Employee:
# ... __init__, name y salary
def monthly_pay(self):
return self._salary
class Manager(Employee):
# ... _bonus y __init__
def monthly_pay(self):
return self.salary + self._bonus
ivan = Employee("Juan", 2000)
maria = Manager("María", 3000, 500)
print(ivan.monthly_pay())
print(maria.monthly_pay())
Salida
2000
3500

Esto se llama sobrescribir un método. La hija escribe un método con el mismo nombre. Para los objetos de la clase hija, su versión reemplaza a la de la clase padre.

No necesitas nada más: no hay palabra clave ni comprobación. Cualquier método se puede sobrescribir. Python solo mira el nombre, no los parámetros. Es cómodo, pero significa que nada impide un error en el nombre o en los parámetros. Enseguida verás qué pasa entonces.

Más precisamente, para curiosos
  • Desde Python 3.12 existe @override del módulo typing. Lo pones encima del método que sobrescribe. Python mismo no lo comprueba: el programa se ejecuta igual con él y sin él. Lo comprueba una herramienta como mypy, que lee el código antes de que lo ejecutes.
  • Tampoco hay forma de prohibir a las clases hijas que cambien un método. @final de typing es solo una nota para esas herramientas.

Pruébalo

¿Qué cambió?

El becario sobrescribe solo el método del pago mensual:

¿Qué imprimirá el programa?

class Intern(Employee):
def monthly_pay(self):
return self.salary // 2
ana = Intern("Ana", 1600)
print(ana.monthly_pay())
print(ana.salary)
Respuesta

La idea

Una lista para todos

Un gerente es un empleado. Así que empleados, gerentes y becarios pueden estar en una lista y tratarse en un solo bucle:

staff = [
Employee("Juan", 2000),
Manager("María", 3000, 500),
Intern("Ana", 1600),
]
for e in staff:
print(f"{e.name}: {e.monthly_pay()} €")
Salida
Juan: 2000 €
María: 3500 €
Ana: 800 €

El bucle llama al mismo método, e.monthly_pay(), y el resultado es distinto. La versión que se ejecuta es la del objeto, no la del tipo de la variable. Esto se llama polimorfismo: en griego, «muchas formas». Una llamada, y cada objeto responde a su manera.

Ahora un nuevo tipo de empleado es una clase nueva con su propio monthly_pay(). El bucle y el resto del programa se quedan como están. No hay ningún if sobre el rol ni un campo de bono en los empleados que no reciben bono.

La variable e no tiene tipo, así que a través de ella puedes escribir lo que quieras, también e.bonus. Si el objeto es un gerente, funciona. Si es un empleado normal, el programa se detiene en esa línea: AttributeError: 'Employee' object has no attribute 'bonus'. Python no comprueba por adelantado. Cómo preguntar a los objetos qué saben hacer está en la lección «Comprobaciones de tipo».

Míralo

Qué método se ejecuta

staff = [
Employee("Juan", 2000),
Manager("María", 3000, 500),
Intern("Ana", 1600),
]
for e in staff:
print(f"{e.name}: {e.monthly_pay()} €")
Paso a paso, en palabras
  1. La lista guarda objetos de tres clases. Cada casilla guarda una flecha.
  2. e apunta a Juan, un Employee normal: se ejecuta el monthly_pay de Employee.
  3. e apunta a María, un Manager: se ejecuta su propio método, salario más bono.
  4. e apunta a Ana, un Intern: el becario recibe la mitad del salario.
  5. La misma línea, tres métodos distintos. Decide el objeto al que apunta e.

La lista guarda tres flechas. Cada una apunta a un objeto de una clase distinta. En cada vuelta e apunta al objeto siguiente, y la llamada va al método de ese objeto: primero en Employee, después en Manager, después en Intern.

La elección se hace mientras el programa se ejecuta. En cada vuelta Python busca monthly_pay primero en la clase del objeto, y después en su clase padre. Nadie comprueba por adelantado que todo objeto tenga ese método. Si alguno no lo tiene, el programa se detiene en su vuelta.

Pruébalo

Encuentra el error

El programa es el mismo, pero el becario recibe el salario completo:

Salida
Juan: 2000 €
María: 3500 €
Ana: 1600 €

Aquí está la clase Intern. ¿Qué línea tiene el error?

Elige la línea con el error.

La idea

Añade a la clase padre, no la reescribas

A veces la hija no quiere reemplazar el método por completo, solo añadirle algo. Un gerente se describe como un empleado, más el bono. Llamas a la versión de la clase padre con super().__str__():

def __str__(self):
return f"{super().__str__()}, bono {self._bonus} €"
print(Employee("Juan", 2000))
print(Manager("María", 3000, 500))
Salida
Juan (2000 €)
María (3000 €), bono 500 €

Si Employee cambia cómo se describe, el gerente lo sigue por sí solo.

Este es también el __str__ del módulo «Clases y objetos». Allí ya sobrescribías un método: la versión de object.

Más precisamente, para curiosos
  • Puedes llamar a super().method() desde cualquier método de la hija, no solo desde el que sobrescribe.
  • Python no compara los parámetros. Si escribes def monthly_pay(self, extra): en Manager, sigue siendo una sobrescritura, y el bucle que llama a e.monthly_pay() se detiene en el gerente: TypeError: Manager.monthly_pay() missing 1 required positional argument: 'extra'. Así que el método que sobrescribe recibe los mismos parámetros que el de la clase padre.

Pruébalo

¿Qué falta?

El becario se describe como un empleado más la palabra «becario». ¿Qué va en el hueco?

class Intern(Employee):
    def __str__(self):
        return f"{.__str__()}, becario"

Ejercicio

Cada uno a su manera

Resuelve las tareas en tu editor. Cada tarea tiene una lista de objetos de varias clases y un solo bucle.

1. Refugio de animales. Una clase Animal con un nombre y un método make_sound() que imprime «algún sonido». Dog y Cat lo sobrescriben. Pon uno de cada uno de los tres en una lista:

Salida
Invitado: algún sonido
Rex: ¡Guau!
Misi: ¡Miau, miau!

2. El estudiante se presenta. Student de la lección anterior ya tiene una nota. Sobrescribe introduce() para que también diga la nota. Usa super().introduce(); no copies la línea de Person.

Salida
Soy Jorge, tengo 45 años.
Soy Elena, tengo 19 años.
Nota media: 5.5

3. Fiesta. Una clase Performance con un título y un método start(). DanceMove, BreakDanceMove (hereda de DanceMove, añade complejidad) y Song lo sobrescriben. Una clase Party guarda una lista de actuaciones y tiene un método start_party() que lo empieza todo en orden y al final dice que la fiesta ha empezado.

Salida
Bailamos moonwalk
Bailamos windmill
complejidad 8 de 10
Cantamos 'Bohemian Rhapsody'
Empieza rifa
¡La fiesta ha empezado!

Más tareas

  • Contratista. Añade a los empleados un Contractor, con horas y una tarifa por hora. Su pago mensual es horas × tarifa. Calcula las nóminas de Juan, María, Ana y Pablo (60 horas a 25 €):
Salida
Total de nóminas: 7800 €
  • Nómina. Dale a Employee un método payslip() que devuelve el texto «nombre: pago €» con el pago mensual. Escríbelo solo en Employee. ¿Por qué funciona bien también para el gerente y para el becario?

Test

Ponte a prueba

1. e = Manager("María", 3000, 500). ¿Qué monthly_pay se ejecuta con e.monthly_pay()?
2. staff = [Employee("Juan", 2000), Manager("María", 3000, 500)]. ¿Qué pasa con print(staff[0].bonus)?
3. ¿Qué tienes que escribir delante de un método que sobrescribe un método de la clase padre?
4. ¿Qué hace super().__str__() dentro del __str__ de la clase hija?
5. Manager tiene def monthly_pay(self, extra):, mientras que Employee no tiene parámetro. El bucle llama a e.monthly_pay() para cada empleado. ¿Qué pasa?

Creado por Ivo Gergov como parte de la campaña de Evo Tech por una educación accesible en programación ·