Contenido del capítulo

Llegamos al siguiente pilar de la programación orientada a objetos, el polimorfismo.

En este capítulo, te explicaré lo que es el polimorfismo y como funciona, con unos cuantos ejemplos prácticos.

Duración estimada de todo el contenido:
Duración del vídeo:
Contiene 5 ejercicios Contiene 1 vídeo.
Tabla de contenidos
Logo

    ¿Qué es el polimorfismo?

    Polimorfismo en clases de Python
    El polimorfismo en Python
    El polimorfismo en Python

    De manera coloquial, se podría decir que estamos ante la habilidad cambia formas de los objetos.

    El polimorfismo es la capacidad de un objeto, de tomar diferentes formas.

    Programación orientada a objetos con Python

    Para que lo entiendas fácilmente, piensa en las personas. Las personas podemos ser muchas cosas, por ejemplo, podríamos ser estudiantes y a la vez trabajadores en una empresa, no tenemos por qué ser solo una cosa; lo que hacemos en un momento determinado, no nos define como personas.

    Programación orientada a objetos con Python

    Entonces, llevando esto a los objetos, nos encontramos con que pueden servir para varios roles diferentes. Estaríamos hablando del polimorfismo.

    Polimorfismo en inglés es polymorphism.

    Espacio publicitario

    Ejemplo de polimorfismo

    Un ejemplo de función que utiliza polimorfismo, es la función predefinida len().

    Esta es capaz de hacer más de una tarea a la vez. Se adapta a diferentes terrenos.

    Por ejemplo, si le pasamos una cadena de caracteres, nos dice la longitud total de caracteres:

    titulo = "Python: el poder de los objetos."
    
    print(len(titulo))
    Resultado en la consola
    32

    En cambio, al utilizar esta función con una tupla o una lista, cuenta los elementos:

    numeros = (10, 50, 345, 43)
    
    print(len(numeros))
    Resultado en la consola
    4

    También, es capaz de contar los pares clave-valor que tiene un diccionario, entre otras posibilidades:

    usuario = {
        "nombre" : "Enrique",
        "apellidos" : "Barros Fernández"
    }
    
    print(len(usuario))
    Resultado en la consola
    2

    Este polimorfismo, lo podemos trasladar a nuestras propias clases, y crear objetos más polifacéticos.

    Clases con polimorfismo

    En el siguiente ejemplo, se utiliza el polimorfismo. Tenemos tres clases que utilizan la herencia de clases y cada una hace su propio uso del mismo método hablar():

    class Animal:
        def hablar(self):
            print("Soy un animal")
    
    class Perro(Animal):
        def hablar(self):
            print("Woof!")
    
    class Gato(Animal):
        def hablar(self):
            print("Meow!")
    
    animal = Animal()
    perro = Perro()
    gato = Gato()
    
    animal.hablar()
    perro.hablar()
    gato.hablar()
    Resultado en la consola
    Soy un animal
    Woof!
    Meow!

    Espacio publicitario

    Polimorfismo con funciones

    El polimorfismo, como has visto con len(), no es exclusivo de los métodos, las funciones también pueden utilizar esta técnica.

    Vamos a crear una función que haga algo parecido. Le pasaremos un objeto, y dependiendo del tipo que sea, actuará de una forma u otra.

    class Animal:
        def hablar(self):
            print("Soy un animal")
    
    class Perro(Animal):
        def hablar(self):
            print("Woof!")
    
    class Gato(Animal):
        def hablar(self):
            print("Meow!")
    
    animal = Animal()
    perro = Perro()
    gato = Gato()
    
    def dar_voz(objeto):
        objeto.hablar()
    
    dar_voz(animal)
    dar_voz(perro)
    dar_voz(gato)
    Resultado en la consola
    Soy un animal
    Woof!
    Meow!

    Esta función es capaz de adaptarse a los diferentes objetos y tener un resultado diferente en torno a qué tipo de objeto se le pasa, como ocurre con len().

    Sobrecarga y sobrescritura

    Anteriormente, en el capítulo de la herencia de clases, he hablado de la sobrescritura. Esta no debe confundirse con la sobrecarga.

    La sobrecarga de métodos es la capacidad que tiene una clase de tener dos o más métodos con el mismo nombre, pero con diferentes parámetros.

    Espacio publicitario

    La sobrescritura de métodos es la capacidad de una subclase, de poder modificar el comportamiento de un método heredado de su superclase. Esto se consigue definiendo un nuevo método con el mismo nombre, mismos parámetros y mismo tipo de retorno.

    Es decir, la sobrescritura añade más “contenido” a una funcionalidad existente. La sobrecarga, varía la funcionalidad.

    Sobrescritura en inglés es overriding, mientras que sobrecarga es overloading.

    La sobrecarga de métodos o funciones no existe como tal en Python. Python, realmente solo tiene en cuenta la última definición del método o función.

    Para que entiendas esto. Mira el siguiente ejemplo:

    # Función con 4 parámetros
    def multiplicacion(a, b, c, d):
        print(a * b * c * d)
    
    multiplicacion(10, 2, 3, 6)
    
    # Funcion con 2 parámetros
    def multiplicacion(a, b):
        print(a * b)
    
    multiplicacion(5, 7)
    Resultado en la consola
    360
    35

    Podemos llamar a los dos métodos. Pero mira lo que ocurre cuando llamamos al método con cuatro parámetros, después de declarar el que lleva dos:

    def multiplicacion(a, b, c, d):
        print(a * b * c * d)
    
    # Funcion con 2 parámetros
    def multiplicacion(a, b):
        print(a * b)
    
    multiplicacion(10, 2, 3, 6)
    Error en la consola
    TypeError: multiplicacion() takes 2 positional arguments but 4 were given
    Error de tipo: La función multiplicacion() espera 2 argumentos posicionales, pero se le proporcionaron 4.

    La segunda función reemplaza a la primera en el flujo de ejecución.

    Sin embargo, la posibilidad de poder tener un método o función que sea capaz de manejar diferentes resultados, en torno a diferente número de parámetros, es una gran ventaja que tenemos en otros lenguajes de programación.

    Por suerte, conseguir esto es posible en Python. Un buen ejemplo de ello es la función range(). Esta puede recibir diferente número de argumentos:

    for i in range(3):
        print(i)
    Resultado en la consola
    0
    1
    2
    for i in range(3, 7):
        print(i)
    Resultado en la consola
    3
    4
    5
    6
    for i in range(3, 35, 7):
        print(i)
    Resultado en la consola
    3
    10
    17
    24
    31

    Para conseguir esto con tus propias funciones o métodos, puedes hacerlo de diferentes formas. Una de ellas, sería esta:

    def multiplicacion(a, b, c=None, d=None):
        if c is not None:
            if d is not None:
                print(a * b * c * d)
            else:
                print(a * b * c)
        else:
            print(a * b)
    
    multiplicacion(45, 34)
    multiplicacion(45, 34, 56)
    multiplicacion(10, 2, 3, 6)
    Resultado en la consola
    1530
    85680
    360

    Ahora puedes multiplicar, mediante la misma función, con diferente número de argumentos; desde dos, hasta cuatro.

    Espacio publicitario

    El problema de esto, es que complica el código innecesariamente, ya que en Python, contamos con una gran herramienta llamada *args, que nos permitirá hacer algo como esto, pero mucho más fácil y mejor.

    Porque ahora, ¿qué ocurre si tienes que multiplicar cinco valores, seis o siete?

    En este código, tendrás que fabricar una estructura laberíntica para conseguirlo, por el hecho de que necesitarás anidar más y más if, según los parámetros que quieras tener.

    Con *args, los argumentos en las llamadas son ilimitados. Un claro ejemplo de esto, es la función predefinida print():

    print(*objects, sep=' ', end='\n', file=None, flush=False)

    Este fragmento de la referencia de Python, muestra como print() utiliza un parámetro especial llamado *objects. El asterisco nos revela que se trata de *args.

    Dedicaré un capítulo entero a mostrarte como funciona este parámetro especial, junto con otro muy parecido, **kwargs.

    Espacio publicitario




    Espacio publicitario


    Ejercicios de Python para resolver

    30. Crea una clase principal. Se llamará Transporte. No añadas más que un método llamado calcular_tiempo(). Este no tendrá parámetros (excepto el que llevan todos los métodos).

    El método servirá para calcular el tiempo que tarda un medio de transporte en llegar, según una distancia. Sin embargo, la funcionalidad la implementaremos desde las subclases.

    En este método, le añadirás simplemente un print() como este:

    print("Accede a este método desde una subclase.")

    Para este ejercicio, solo tenías que seguir las instrucciones.

    class Transporte(object):
        def calcular_tiempo(self):
            print("Accede a este método desde una subclase.")

    Ahora, si alguien intenta acceder al método, sabrá que este está realmente implementado en las subclases, gracias a ese aviso en el print().

    31. Añade dos subclases de la superclase Transporte. Coche y Bicicleta.

    Estas subclases no tendrán método __init__, e implementarán el método calcular_tiempo() de la superclase.

    Esta vez, el método aceptará un argumento de entrada llamado distancia.

    Por el momento, deja este método con pass en ambas clases, hasta recibir nuevas instrucciones.

    Preparamos las clases para implementarles el código en el siguiente ejercicio.

    class Coche(Transporte):
        def calcular_tiempo(self, distancia):
            pass
        
    class Bicicleta(Transporte):
        def calcular_tiempo(self, distancia):
            pass

    32. Cambia el pass de los dos métodos, y aplica la siguiente fórmula a los vehículos (Coche y Bicicleta):

    Duración = distancia / velocidad
    • Coche tendrá una velocidad de 100 km/h.
    • Bicicleta tendrá una velocidad de 17 km/h.
    • Representa estas velocidades con un tipo int.
    • Después, crea dos objetos. Uno de tipo Coche, y otro de tipo Bicicleta.
    • Con cada uno, llama al método calcular_tiempo(), con un valor cualquiera de distancia. Por ejemplo, 50 km (lo puedes representar con int o float si quieres decimales).
    • Después de hacer el cálculo, se deberá imprimir un mensaje como este:

    • La bicicleta tardará 2.94 horas en recorrer 50 km a 17 km/h.
    • Opcionalmente, puedes quitar decimales del resultado con la función round() en el cálculo.

    Como puedes ver, la clase Transporte es la que tiene inicialmente un método de calcular_tiempo(), que sirve para que sus clases derivadas, implementen con su propia fórmula. Mismo método, resultados diferentes según el tipo de objeto.

    class Transporte:
        def calcular_tiempo(self):
            print("Accede a este método desde una subclase.")
    
    class Coche(Transporte):
        def calcular_tiempo(self, distancia):
            velocidad = 100
            tiempo = distancia / velocidad
            print(f"El coche tardará {tiempo} horas en recorrer {distancia} km a {velocidad} km/h")
        
    class Bicicleta(Transporte):
        def calcular_tiempo(self, distancia):
            velocidad = 17
            duracion = round(distancia / velocidad , 2)
            print(f"La bicicleta tardará {duracion} horas en recorrer {distancia} km a {velocidad} km/h")
    
    vehiculo_1 = Bicicleta()
    vehiculo_2 = Coche()
    
    vehiculo_1.calcular_tiempo(50)
    vehiculo_2.calcular_tiempo(50)
    Resultado en la consola
    La bicicleta tardará 2.94 horas en recorrer 50 km a 17 km/h
    El coche tardará 0.5 horas en recorrer 50 km a 100 km/h

    33. Ahora, sería interesante poder añadir también un parámetro, para que se pase también la velocidad del Vehiculo, en la llamada del método calcular_tiempo().

    Gracias al nuevo parámetro, podemos calcular diferentes distancias a diferentes velocidades, con diferentes transportes.

    class Transporte:
        def calcular_tiempo(self):
            print("Accede a este método desde una subclase.")
    
    class Coche(Transporte):
        def calcular_tiempo(self, distancia, velocidad):
            tiempo = round(distancia / velocidad, 2)
            print(f"El coche tardará {tiempo} horas en recorrer {distancia} km a {velocidad} km/h")
        
    class Bicicleta(Transporte):
        def calcular_tiempo(self, distancia, velocidad):
            duracion = round(distancia / velocidad , 2)
            print(f"La bicicleta tardará {duracion} horas en recorrer {distancia} km a {velocidad} km/h")
    
    vehiculo_1 = Bicicleta()
    vehiculo_2 = Coche()
    
    vehiculo_1.calcular_tiempo(100,10)
    vehiculo_2.calcular_tiempo(100,180.6)
    Resultado en la consola
    La bicicleta tardará 2.94 horas en recorrer 50 km a 17 km/h
    El coche tardará 0.5 horas en recorrer 50 km a 100 km/h

    Cuando llegues a la validación de datos, podrás hacer incluso una validación de rangos, en la que especifiques que cada objeto de vehículo, tenga su propia velocidad mínima y máxima.

    Por ejemplo, que no se pueda llamar al método calcular_tiempo(), para un objeto de tipo Bicicleta, y se le dé un argumento de velocidad a 500 km/h. Cosa que no tendría ningún sentido.

    Espacio publicitario

    34. La llamada es poco identificativa. Si no hubieses hecho estos ejercicios, ¿sabrías a simple vista decirme para qué es cada argumento?

    vehiculo_2.calcular_tiempo(100,180.6)

    La respuesta es que solo podrías sacar deducciones, si no consultas directamente la declaración del método.

    Quiero que apliques los argumentos de clave en las dos llamadas del ejercicio anterior.

    La sintaxis de los argumentos utilizados hasta ahora es esta:

    Argumento posicional:

    Método(valor)

    La que quiero que apliques es esta:

    Argumento de clave:

    Método(atributo=valor)

    De esta forma se sabe muy bien en la llamada, a qué parámetro corresponde cada valor.

    vehiculo_1.calcular_tiempo(distancia=1,velocidad=19)
    vehiculo_2.calcular_tiempo(distancia=1700,velocidad=90)

    Además, podemos cambiar el orden posicional:

    vehiculo_1.calcular_tiempo(velocidad=19, distancia=1)
    vehiculo_2.calcular_tiempo(distancia=1700,velocidad=90)


    Espacio publicitario