POO: Polimorfismo en Python

Publicado por Andrea Navarro el

En este artículo veremos qué es el polimorfismo y cuál es su función en la programación orientada a objetos. Veremos el polimorfismo con y sin herencia y cómo implementarlo correctamente.

Polimorfismo

En la programación orientada a objetos (POO), el polimorfismo es uno de los cuatro principios fundamentales, junto con la encapsulación, la abstracción y la herencia.

El polimorfismo permite la definición de interfaces para operaciones que tienen diferentes comportamientos dependiendo del tipo de objeto que lo ejecuta. Su uso evita tener que utilizar estructuras condicionales que verifican tipos reduciendo la complejidad del código y haciendo el código más mantenible y flexible.

Implementar polimorfismo hace que el programa interactúe con abstracciones en lugar de con implementaciones. Esto permite que se puedan añadir nuevos comportamientos mediante la creación de nuevas clases, sin modificar el código ya existente.

Polimorfismo con herencia

En el siguiente caso se muestra un caso de polimorfismo con herencia, supongamos que tenemos diferentes tipos de empleados en una empresa, todos tienen datos básicos compartidos y un método en común llamado calcular_salario.

La implementación de este método es diferente para cada tipo de empleado, y es se calcula a partir de datos que son particulares para cada tipo.

En el código que se encuentra a continuación se ha creado una clase base Empleado, que tendrá los atributos comunes a todos los tipos de empleado, e implementa calcular_salario. Cada tipo de empleado existente tiene su propia clase que hereda de la clase base Empleado estableciendo sus propios atributos y la implementación específica del método calcular_salario.

class Empleado:
    def __init__(self, nombre: str, salario_base: float):
        self.nombre = nombre
        self.salario_base = salario_base

    def calcular_salario(self) -> float:
        raise NotImplementedError("Las subclases deben implementar el método calcular_salario")

class EmpleadoTiempoCompleto(Empleado):
    def __init__(self, nombre: str, salario_base: float):
        super().__init__(nombre, salario_base)

    def calcular_salario(self) -> float:
       return self.salario_base

class EmpleadoPorHora(Empleado):
    def __init__(self, nombre: str, salario_base: float, horas_trabajadas: int):
        super().__init__(nombre, salario_base)
        self.horas_trabajadas = horas_trabajadas

    def calcular_salario(self) -> float:
        return self.salario_base * self.horas_trabajadas

class EmpleadoPracticante(Empleado):
    def __init__(self, nombre: str, salario_base: float, bono_desempeno: float = 0):
        super().__init__(nombre, salario_base)
        self.bono_desempeno = bono_desempeno

    def calcular_salario(self) -> float:
        return self.salario_base + self.bono_desempeno

Si se quiere realizar una operación como calcular la nómina total, es posible recorrer una lista de empleados de todo tipo, y ejecutar el método calcular_salario.

Dependiendo del empleado que se esté utilizando se ejecutará el método correspondiente, haciendo uso de la sobreescritura de métodos.

empleados = [
    EmpleadoTiempoCompleto("Ana García", 3000),
    EmpleadoPorHora("Carlos López", 15.50, 160),
    EmpleadoTiempoCompleto("David Torres", 3200),
    EmpleadoPorHora("Elena Castro", 18.75, 120),
]
total_nomina = 0
for empleado in empleados:
    salario = empleado.calcular_salario()
    total_nomina += salario
print(f"TOTAL NÓMINA: ${total_nomina:.2f}")
TOTAL NÓMINA: $10930.00

Si se quiere agregar un nuevo tipo de empleado con nuevos atributos y otra forma de calcular su salario solo es necesario agregar una nueva clase que herede de la clase Empleado, y sobreescribir su método calcular_salario. El resto del código no requiere modificaciones.

Duck typing o tipado dinámico

Python permite implementar el polimorfismo mediante un enfoque basado en tipado dinámico estructuralduck typing.

Bajo este enfoque no es necesario generar herencias entre clases ni que estas dependen unas de otras. Esto permite más flexibilidad, ya que lo único que se tiene en cuenta es en sí las clases comparten o no métodos comunes.

En este caso se tienen diferentes manejadores para distintas partes del sistema (Base de Datos, Archivos JSON y Cache). Aunque todas estas clases tienen sus propios atributos y métodos que no son compartidos entre ellos si tienen en común los métodos cargar y descargar. Las clases no heredan de una clase base ni se relacionan entre sí, pero pueden utilizarse los métodos mencionados para crear polimorfismo.

La función carga_descarga_datos recibe como argumento un tipo de clase usada para almacenamiento, junto con otros datos, y ejecuta los métodos cargar y descargar del objeto correspondiente.

class ManejadorBaseDatos:
    # Métodos y operaciones propias
    def guardar(self, datos):
        print(f"Guardando en BD: {datos}")

    def cargar(self, id):
        return f"Datos desde BD para {id}"

class ManejadorArchivoJSON:
    # Métodos y operaciones propias
    def guardar(self, datos):
        print(f"Guardando en JSON: {datos}")

    def cargar(self, ruta):
        return f"Datos desde JSON en {ruta}"

class ManejadorCacheMemoria:
    # Métodos y operaciones propias
    def guardar(self, datos):
        print(f"Guardando en cache: {datos}")

    def cargar(self, clave):
        return f"Datos desde cache con clave {clave}"

def carga_descarga_datos(almacenamiento, datos):
    almacenamiento.guardar(datos)
    return almacenamiento.cargar("id123")

Se puede ver en este ejemplo que, aunque las clases son diferentes y han sido creadas para un objetivo específico, pueden ejecutarse los métodos de manera indistinta.

carga_descarga_datos(ManejadorBaseDatos(), {"a": 1})
carga_descarga_datos(ManejadorArchivoJSON(), {"b": 2})
carga_descarga_datos(ManejadorCacheMemoria(), {"c": 3})
Guardando en BD: {'a': 1}
Guardando en JSON: {'b': 2}
Guardando en cache: {'c': 3}

Aunque este método provee mucha flexibilidad, no existe una relación explícita entre las clases, esto hace la documentación más compleja, y es más probable la aparición de errores cuando un método no ha sido implementado correctamente.

Buenas prácticas

La implementación del polimorfismo requiere seguir un conjunto de principios de diseño para evitar errores.

En primer lugar, es necesario programar por comportamientos y no para tipos concretos, es decir, el código debe interactuar con interfaces abstractas definidas por comportamientos esperados. Esto reduce el acoplamiento entre componentes, y permite la integración de nuevos tipos sin la necesidad de modificar el código existente.

Uno de los aspectos más importantes a tener en cuenta es que las interfaces deben ser coherentes. Los métodos polimórficos deben ser consistentes y uniformes entre si. Se debe respetar no solo el nombre y parámetros del mismo, sino también su funcionalidad, pre-condiciones y post-condiciones.

Se deben evitar herencias de más de tres niveles ya que aumentan la complejidad y reducen la claridad del código. El uso de mixins es recomendado para reutilizar comportamiento sin crear jerarquías.


En este artículo hemos visto qué es le polimorfismo y cómo permite la flexibilización y reutilización de código. Hemos explorado como aplicarlo haciendo uso de herencia y de duck typing, y cómo implementarlo correctamente para evitar errores.


¿Querés aprender más? 📚

👉 Visitá nuestros cursos!
💬 Y si tenés dudas, o querés dejarnos tus comentarios sumate a la Comunidad JuncoTIC en Telegram!
¡Te esperamos!

Categorías: Programación

Andrea Navarro

- Ingeniera en Informática - Docente universitaria - Investigadora