¿Qué es el polimorfismo?


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.

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.

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))
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))
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))
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()
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)
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)
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)
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)
0
1
2
for i in range(3, 7):
print(i)
3
4
5
6
for i in range(3, 35, 7):
print(i)
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)
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
):
-
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 tipoBicicleta
. -
Con cada uno, llama al método
calcular_tiempo()
, con un valor cualquiera de distancia. Por ejemplo, 50 km (lo puedes representar conint
ofloat
si quieres decimales). - Después de hacer el cálculo, se deberá imprimir un mensaje como este:
-
Opcionalmente, puedes quitar decimales del resultado con
la función
round()
en el cálculo.
La bicicleta tardará 2.94 horas en recorrer 50 km a 17 km/h.
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)
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)
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