27- Polymorphism en Python




Python Polymorphism

🎭 Python Polymorphism: Muchas Formas, Una Acción

El **polimorfismo** es un concepto fundamental en la programación orientada a objetos (POO) que significa "muchas formas". En el contexto de la programación, se refiere a la capacidad de diferentes objetos de responder al mismo método o función de manera específica a su tipo.

En esencia, el polimorfismo permite escribir código que puede funcionar con objetos de diferentes clases sin necesidad de conocer el tipo exacto de cada objeto en tiempo de ejecución. Esto conduce a un código más flexible, reutilizable y fácil de mantener.

Tipos de Polimorfismo en Python:

Python implementa el polimorfismo de varias maneras, incluyendo:

  • Polimorfismo de función: Funciones que pueden operar con objetos de diferentes tipos.
  • Polimorfismo de clase (a través de la herencia): Métodos en clases hijas que pueden sobrescribir o extender los métodos de sus clases padres, comportándose de manera diferente.

Ejemplo Sencillo de Polimorfismo de Función: La Función len()

Una función incorporada en Python que demuestra el polimorfismo es len(). Esta función puede tomar diferentes tipos de objetos (como cadenas, listas, tuplas, diccionarios, conjuntos) y devuelve la "longitud" apropiada para cada tipo:

      
mi_cadena = "Hola"
mi_lista = [1, 2, 3, 4]
mi_tupla = (10, 20, 30)
mi_diccionario = {"a": 1, "b": 2}
mi_conjunto = {5, 6, 7}

print(len(mi_cadena))      # Output: 4 (número de caracteres)
print(len(mi_lista))       # Output: 4 (número de elementos)
print(len(mi_tupla))       # Output: 3 (número de elementos)
print(len(mi_diccionario)) # Output: 2 (número de pares clave-valor)
print(len(mi_conjunto))    # Output: 3 (número de elementos únicos)
      
     

La función len() funciona de manera polimórfica porque la misma llamada de función (len()) produce resultados diferentes basados en el tipo del objeto que se le pasa como argumento. Cada tipo de objeto implementa internamente la lógica necesaria para calcular su propia longitud.

En los siguientes temas, exploraremos el polimorfismo con diferentes tipos de datos y a través de la herencia de clases.

Function Polymorphism

⚙️ Polimorfismo de Función: Una Función, Múltiples Comportamientos

El polimorfismo de función se manifiesta cuando una función puede operar sobre objetos de diferentes clases sin necesidad de conocer explícitamente el tipo de cada objeto. La clave para esto radica en que los objetos deben implementar los métodos que la función espera utilizar, aunque la implementación de esos métodos puede variar entre las clases.

Ejemplo: Una Función para Presentar Objetos

Consideremos una función simple llamada presentar() que toma un objeto como argumento y llama a un método llamado hablar() en ese objeto. Diferentes clases de objetos pueden tener su propia implementación del método hablar(), lo que resulta en un comportamiento polimórfico de la función presentar().

      
class Gato:
    def hablar(self):
        print("¡Miau!")

class Perro:
    def hablar(self):
        print("¡Guau!")

class Pato:
    def hablar(self):
        print("¡Cuac!")

def presentar(animal):
    animal.hablar()

# Crear instancias de las clases
mi_gato = Gato()
mi_perro = Perro()
mi_pato = Pato()

# Llamar a la misma función con diferentes objetos
presentar(mi_gato)  # Output: ¡Miau!
presentar(mi_perro) # Output: ¡Guau!
presentar(mi_pato)  # Output: ¡Cuac!
      
     

En este ejemplo, la función presentar() no sabe si el objeto animal es un Gato, un Perro o un Pato. Sin embargo, como cada una de estas clases implementa un método llamado hablar(), la función presentar() puede llamar a ese método en cualquier objeto de estas clases, y cada objeto responderá de acuerdo a su propia implementación.

El Poder del Polimorfismo de Función:

  • Flexibilidad: Permite que el código funcione con una variedad de objetos.
  • Reusabilidad: La misma función se puede utilizar para diferentes tipos, reduciendo la necesidad de escribir funciones específicas para cada tipo.
  • Extensibilidad: Se pueden añadir nuevas clases que implementen el método esperado por la función, y la función seguirá funcionando con los objetos de estas nuevas clases sin necesidad de modificación.

Polimorfismo con Operadores:

Otro ejemplo de polimorfismo de función en Python se ve con los operadores. Por ejemplo, el operador + puede realizar diferentes acciones dependiendo de los tipos de operandos:

      
print(2 + 3)       # Output: 5 (suma de números)
print("Hola" + " " + "Mundo") # Output: Hola Mundo (concatenación de cadenas)
print([1, 2] + [3, 4])     # Output: [1, 2, 3, 4] (concatenación de listas)
      
     

El operador + está sobrecargado (internamente implementado de forma diferente) para trabajar con números, cadenas y listas, mostrando un comportamiento polimórfico.

En los siguientes temas, exploraremos ejemplos de polimorfismo con diferentes tipos de datos incorporados en Python.

String

🧵 Polimorfismo con Cadenas (str)

Las cadenas en Python son secuencias inmutables de caracteres y soportan varias operaciones que demuestran polimorfismo, especialmente cuando interactúan con funciones y operadores que también funcionan con otros tipos de secuencia.

Ejemplo: La Función len()

Como vimos en la introducción al polimorfismo, la función len() se comporta de manera polimórfica con las cadenas, devolviendo el número de caracteres que contiene la cadena.

      
mi_string = "Python"
print(len(mi_string)) # Output: 6
      
     

La misma función len() se utiliza para obtener la longitud de listas, tuplas, etc., mostrando su naturaleza polimórfica.

Ejemplo: El Operador + (Concatenación)

El operador +, cuando se utiliza con cadenas, realiza la operación de concatenación, uniendo dos o más cadenas en una nueva cadena.

      
parte1 = "Hola"
parte2 = "Mundo"
saludo = parte1 + " " + parte2
print(saludo) # Output: Hola Mundo
      
     

Este mismo operador + se comporta de manera diferente con números (suma) y listas/tuplas (concatenación de secuencias), lo que también es un ejemplo de polimorfismo de operador.

Ejemplo: Métodos Comunes a Secuencias

Aunque las cadenas tienen sus propios métodos específicos, también comparten algunos métodos conceptualmente similares a otras secuencias (como listas y tuplas), aunque la implementación interna sea diferente. Un ejemplo podría ser la idea de "contener" elementos (caracteres en el caso de cadenas).

      
mi_string = "abcdefg"
print('c' in mi_string) # Output: True
print('z' in mi_string) # Output: False

mi_lista = [1, 2, 3, 4]
print(3 in mi_lista)   # Output: True
print(5 in mi_lista)   # Output: False
      
     

El operador in funciona con diferentes tipos de secuencias (cadenas y listas en este caso) para verificar la pertenencia de un elemento, mostrando un comportamiento polimórfico a nivel de operador.

La Naturaleza Inmutable de las Cadenas:

Es importante recordar que las cadenas en Python son inmutables. Las operaciones que parecen modificar una cadena (como la concatenación o el uso de métodos como replace()) en realidad crean una nueva cadena en lugar de modificar la original. Este comportamiento es específico del tipo str.

En el siguiente tema, veremos cómo el polimorfismo se aplica a las tuplas en Python.

Tuple

🧱 Polimorfismo con Tuplas (tuple)

Las tuplas en Python son secuencias inmutables de elementos. Al igual que las cadenas y las listas, las tuplas exhiben polimorfismo al interactuar con funciones y operadores diseñados para trabajar con secuencias.

Ejemplo: La Función len()

La función len() funciona de manera polimórfica con las tuplas, devolviendo el número de elementos que contiene la tupla.

      
mi_tupla = (1, 2, 3, "a", "b")
print(len(mi_tupla)) # Output: 5
      
     

Esta misma función, como ya hemos visto, también calcula la longitud de cadenas y listas, demostrando su comportamiento polimórfico.

Ejemplo: El Operador + (Concatenación)

El operador +, cuando se utiliza con tuplas, realiza la operación de concatenación, creando una nueva tupla que contiene los elementos de las tuplas originales.

      
tupla1 = (10, 20)
tupla2 = (30, 40, 50)
tupla_combinada = tupla1 + tupla2
print(tupla_combinada) # Output: (10, 20, 30, 40, 50)
      
     

Al igual que con las cadenas y las listas, el operador + se comporta de manera diferente con las tuplas, lo que es un ejemplo de polimorfismo de operador.

Ejemplo: El Operador in (Pertenencia)

El operador in se utiliza para verificar si un elemento está presente dentro de una tupla, funcionando de manera similar a cómo lo hace con cadenas y listas.

      
mi_tupla = ('manzana', 'banana', 'cereza')
print('banana' in mi_tupla) # Output: True
print('uva' in mi_tupla)    # Output: False

mi_lista = [1, 2, 3]
print(2 in mi_lista)       # Output: True
      
     

La consistencia en el uso del operador in a través de diferentes tipos de secuencia es un claro ejemplo de polimorfismo.

La Inmutabilidad de las Tuplas:

Es importante recordar que, al igual que las cadenas, las tuplas son inmutables. Las operaciones que parecen modificarlas (como la concatenación) en realidad crean una nueva tupla.

En el siguiente tema, exploraremos el polimorfismo en el contexto de los diccionarios en Python.

Dictionary

🔑 Polimorfismo con Diccionarios (dict)

Los diccionarios en Python son colecciones de pares clave-valor. Aunque no son secuencias ordenadas como cadenas o tuplas, también participan en el polimorfismo a través de funciones y métodos que operan sobre ellos de manera consistente.

Ejemplo: La Función len()

La función len(), cuando se aplica a un diccionario, devuelve el número de pares clave-valor (ítems) que contiene el diccionario.

      
mi_diccionario = {"nombre": "Ana", "edad": 30, "ciudad": "Madrid"}
print(len(mi_diccionario)) # Output: 3
      
     

Una vez más, la misma función len() se adapta al tipo de objeto, mostrando su comportamiento polimórfico.

Ejemplo: El Operador in (Pertenencia de Claves)

El operador in, cuando se utiliza con un diccionario, verifica si una clave específica existe en el diccionario.

      
mi_diccionario = {"a": 1, "b": 2, "c": 3}
print("b" in mi_diccionario) # Output: True
print(1 in mi_diccionario)   # Output: False (verifica claves, no valores)

mi_lista = [1, 2, 3]
print(2 in mi_lista)       # Output: True
      
     

Aunque la semántica de "pertenencia" es diferente (claves en diccionarios, elementos en listas), el operador in se utiliza de forma consistente, lo que puede considerarse una forma de polimorfismo a nivel de operador.

Ejemplo: Métodos Comunes (Conceptualmente)

Los diccionarios tienen métodos como keys(), values() y items() que devuelven vistas (objetos iterables) de las claves, los valores y los pares clave-valor respectivamente. La idea de iterar sobre los "elementos" de una colección es común a diferentes tipos, aunque la naturaleza de esos "elementos" varíe.

      
mi_diccionario = {"nombre": "Carlos", "profesion": "Ingeniero"}
for clave in mi_diccionario.keys():
    print(clave)
# Output:
# nombre
# profesion

for valor in mi_diccionario.values():
    print(valor)
# Output:
# Carlos
# Ingeniero
      
     

La capacidad de iterar sobre los contenidos de un diccionario de manera similar a cómo se itera sobre los elementos de una lista o los caracteres de una cadena es un ejemplo de polimorfismo a nivel de comportamiento.

La Naturaleza de Mapeo de los Diccionarios:

Es crucial recordar que los diccionarios son mapeos, no secuencias ordenadas (hasta Python 3.7). Su comportamiento polimórfico se manifiesta en las operaciones que son significativas para su estructura de clave-valor.

En el siguiente tema, exploraremos el polimorfismo de clase.

Class Polymorphism

🏢 Polimorfismo de Clase: Objetos con la Misma Interfaz, Diferentes Implementaciones

El polimorfismo de clase ocurre cuando diferentes clases definen métodos con el mismo nombre, pero estos métodos realizan acciones que son específicas para cada clase. Esto permite tratar objetos de diferentes clases de manera uniforme si comparten una interfaz común (un conjunto de métodos con los mismos nombres).

Ejemplo: Figuras Geométricas

Consideremos diferentes clases que representan figuras geométricas, como un Circulo y un Cuadrado. Ambas figuras pueden tener un método para calcular su área, pero la forma en que se calcula el área es diferente para cada figura.

      
import math

class Circulo:
    def __init__(self, radio):
        self.radio = radio

    def calcular_area(self):
        return math.pi * self.radio**2

class Cuadrado:
    def __init__(self, lado):
        self.lado = lado

    def calcular_area(self):
        return self.lado**2

def mostrar_area(figura):
    print(f"El área de la figura es: {figura.calcular_area()}")

# Crear instancias de las clases
mi_circulo = Circulo(5)
mi_cuadrado = Cuadrado(4)

# Llamar a la misma función con diferentes objetos
mostrar_area(mi_circulo)  # Output: El área de la figura es: 78.53981633974483
mostrar_area(mi_cuadrado) # Output: El área de la figura es: 16.0
      
     

En este ejemplo:

  • Tanto la clase Circulo como la clase Cuadrado tienen un método llamado calcular_area().
  • La función mostrar_area() toma un objeto (que esperamos que tenga un método calcular_area()) y llama a ese método sin preocuparse por el tipo específico de la figura.
  • El resultado de calcular_area() es diferente para un círculo y un cuadrado, lo que demuestra el polimorfismo de clase.

Beneficios del Polimorfismo de Clase:

  • Abstracción: Permite trabajar con objetos a través de una interfaz común, ocultando los detalles de implementación específicos de cada clase.
  • Flexibilidad: Facilita la adición de nuevas clases que compartan la misma interfaz, sin necesidad de modificar el código que utiliza esa interfaz.
  • Mantenibilidad: El código se vuelve más organizado y fácil de entender, ya que las responsabilidades se distribuyen entre las diferentes clases.

Duck Typing en Python:

Python implementa el polimorfismo a través de un concepto conocido como "duck typing". Si un objeto "camina como un pato y grazna como un pato", entonces se le trata como un pato. En otras palabras, lo que importa no es la clase del objeto, sino si implementa los métodos y atributos que se esperan.

En el ejemplo anterior, mientras los objetos mi_circulo y mi_cuadrado tengan un método calcular_area(), la función mostrar_area() podrá trabajar con ellos, independientemente de su tipo específico.

En el siguiente tema, veremos cómo la herencia potencia aún más el polimorfismo de clase.

Inheritance Class Polymorphism

🧬 Polimorfismo de Clase a Través de la Herencia: Un Comportamiento Común en una Jerarquía

Cuando una clase hereda de una clase padre, hereda sus métodos y atributos. El polimorfismo a través de la herencia se da cuando una clase hija **sobrescribe** (define un método con el mismo nombre que en la clase padre) un método heredado o implementa un método definido en una interfaz o clase abstracta padre. Esto permite que objetos de diferentes clases en la misma jerarquía respondan al mismo método de manera específica a su tipo.

Ejemplo: Animales Hablando

Consideremos una clase padre llamada Animal con un método hablar(). Luego, creamos clases hijas como Perro y Gato que heredan de Animal y sobrescriben el método hablar() para producir el sonido característico de cada animal.

      
class Animal:
    def hablar(self):
        print("Sonido genérico de animal")

class Perro(Animal):
    def hablar(self):
        print("¡Guau!")

class Gato(Animal):
    def hablar(self):
        print("¡Miau!")

def hacer_hablar(animal):
    animal.hablar()

# Crear instancias de las clases hijas
mi_perro = Perro()
mi_gato = Gato()

# Tratar objetos de diferentes clases de manera uniforme
hacer_hablar(mi_perro) # Output: ¡Guau!
hacer_hablar(mi_gato)  # Output: ¡Miau!

# También podemos tratar a los objetos como instancias de la clase padre
un_animal_perro = Perro()
un_animal_gato = Gato()

hacer_hablar(un_animal_perro) # Output: ¡Guau!
hacer_hablar(un_animal_gato)  # Output: ¡Miau!
      
     

En este ejemplo:

  • La clase Animal define una interfaz común con el método hablar().
  • Las clases hijas Perro y Gato proporcionan implementaciones específicas del método hablar().
  • La función hacer_hablar() puede tomar como argumento cualquier objeto que sea una instancia de Animal o de cualquiera de sus clases hijas, y llamará al método hablar() específico de ese objeto.

Beneficios del Polimorfismo con Herencia:

  • Reutilización de código: La clase padre proporciona una base común que las clases hijas pueden extender o modificar.
  • Extensibilidad: Se pueden añadir nuevas clases hijas que hereden de la clase padre y proporcionen su propia implementación de los métodos polimórficos, sin necesidad de modificar el código existente que utiliza la clase padre.
  • Mantenibilidad: Los cambios en el comportamiento específico de una clase se limitan a esa clase, lo que facilita el mantenimiento del código.

Uso de super() en el Polimorfismo:

En métodos sobrescritos, a menudo se utiliza la función super() para llamar a la implementación del método en la clase padre antes o después de añadir un comportamiento específico en la clase hija. Esto permite extender el comportamiento heredado en lugar de reemplazarlo por completo.

Con esto, hemos explorado cómo la herencia y el polimorfismo trabajan juntos para crear sistemas de objetos flexibles y extensibles en Python.




Publicar un comentario

0 Comentarios