Polimorfismo en Python
Aprende el polimorfismo en Python: sobrescritura de métodos, duck typing, sobrecarga de operadores e interfaces abstractas con ejemplos claros.
El polimorfismo (del griego "muchas formas") permite que un mismo fragmento de código funcione con objetos de distintos tipos, siempre que esos objetos soporten la interfaz esperada. Se llama al mismo nombre de método en cada objeto, y cada objeto responde según su propia implementación.
El polimorfismo es uno de los cuatro pilares de la programación orientada a objetos, junto con la encapsulación, la herencia y la abstracción. Es lo que permite que las funciones que aceptan un tipo de clase base funcionen automáticamente con cualquier subclase.
Este capítulo cubre:
- Qué es el polimorfismo y por qué es importante
- Polimorfismo mediante sobrescritura de métodos
- Polimorfismo mediante duck typing
- Sobrecarga de operadores — una forma de polimorfismo integrada en Python
- Clases base abstractas como forma formal de definir interfaces polimórficas
- Patrones prácticos y errores comunes
Antes de leer este capítulo, asegúrate de estar familiarizado con las clases y objetos de Python y la herencia en Python.
Por qué importa el polimorfismo
Sin polimorfismo, una función que trabaja con animales necesitaría una cadena explícita de if/elif para cada tipo de animal:
def make_sound(animal):
if type(animal).__name__ == "Dog":
print("Woof!")
elif type(animal).__name__ == "Cat":
print("Meow!")
elif type(animal).__name__ == "Bird":
print("Tweet!")
# ... add a new branch every time you add a new animal typeEso es frágil. Cada nuevo tipo de animal requiere modificar esta función. Con polimorfismo se escribe:
def make_sound(animal):
animal.speak() # works for any object that has a speak() methodAgregar un nuevo tipo de animal solo requiere definir su método speak() — la función en sí nunca cambia. Este es el principio abierto/cerrado: abierto para extensión, cerrado para modificación.
Polimorfismo mediante sobrescritura de métodos
La forma más común de polimorfismo en Python es la sobrescritura de métodos: una subclase proporciona su propia versión de un método que fue definido por la clase padre.
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
return f"{self.name} makes a sound."
class Dog(Animal):
def speak(self):
return f"{self.name} says woof!"
class Cat(Animal):
def speak(self):
return f"{self.name} says meow!"
class Bird(Animal):
def speak(self):
return f"{self.name} says tweet!"Ahora un único bucle funciona para los tres tipos:
animals = [Dog("Rex"), Cat("Whiskers"), Bird("Tweety")]
for animal in animals:
print(animal.speak())
# Rex says woof!
# Whiskers says meow!
# Tweety says tweet!animal.speak() despacha a la versión correcta en tiempo de ejecución según el tipo real del objeto. Este despacho en tiempo de ejecución se denomina despacho dinámico o enlace tardío.
Extender versus reemplazar el método padre
Al sobrescribir, puedes reemplazar completamente el comportamiento del padre o extenderlo usando super():
class Animal:
def speak(self):
print("[Animal vocalization]")
class Dog(Animal):
def speak(self):
super().speak() # keep the parent's output
print("Woof! (Dog override adds this)")
Dog().speak()
# [Animal vocalization]
# Woof! (Dog override adds this)Usa super() cuando la versión del padre realiza una configuración o registro útil que aún debe ejecutarse. Omítelo cuando quieras reemplazar el comportamiento por completo.
Polimorfismo mediante duck typing
El sistema de tipos de Python es estructural y no nominal. Un objeto no tiene que pertenecer a una jerarquía de clases determinada; simplemente necesita tener los métodos correctos. Esto se llama duck typing — nombrado así por el dicho "si camina como un pato y grazna como un pato, entonces es un pato".
class Dog:
def speak(self):
return "Woof!"
class Robot:
def speak(self):
return "Beep boop."
class Human:
def speak(self):
return "Hello!"
def introduce(entity):
print(entity.speak())
introduce(Dog()) # Woof!
introduce(Robot()) # Beep boop.
introduce(Human()) # Hello!Dog, Robot y Human no comparten ninguna clase padre común (más allá del object incorporado). Sin embargo, introduce() funciona con los tres porque cada uno tiene un método speak(). La función no verifica qué tipo es entity — simplemente llama al método y confía en que el objeto responderá correctamente.
Cuándo usar duck typing versus herencia
| Situación | Enfoque preferido |
|---|---|
| Los objetos están relacionados lógicamente (todos son animales) | Jerarquía de herencia |
| Los objetos no están relacionados pero comparten un comportamiento | Duck typing |
| Quieres aplicar la interfaz en el momento de la definición | Clases base abstractas |
| Se trabaja con tipos integrados o clases de terceros que no puedes cambiar | Duck typing |
El duck typing es idiomático en Python y se usa ampliamente en la biblioteca estándar — por ejemplo, len() funciona en cualquier object que defina __len__, independientemente de su clase.
Polimorfismo con funciones y bucles
Puedes escribir una única función que trate distintos tipos de forma uniforme mediante el polimorfismo. Considera una aplicación de dibujo:
class Circle:
def __init__(self, radius):
self.radius = radius
def area(self):
import math
return math.pi * self.radius ** 2
def describe(self):
return f"Circle with radius {self.radius}"
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
def describe(self):
return f"Rectangle {self.width}x{self.height}"
class Triangle:
def __init__(self, base, height):
self.base = base
self.height = height
def area(self):
return 0.5 * self.base * self.height
def describe(self):
return f"Triangle base={self.base} height={self.height}"
shapes = [Circle(5), Rectangle(4, 6), Triangle(3, 8)]
for shape in shapes:
print(f"{shape.describe()}: area = {shape.area():.2f}")
# Circle with radius 5: area = 78.54
# Rectangle 4x6: area = 24.00
# Triangle base=3 height=8: area = 12.00El bucle llama a area() y describe() en cada figura sin importar a qué clase pertenece el objeto. Agregar una clase Pentagon más adelante solo requiere escribir la nueva clase — el bucle no cambia.
Sobrecarga de operadores
Los operadores aritméticos y de comparación de Python también son polimórficos. + sobre enteros suma números; + sobre strings los concatena; + sobre listas los fusiona. Python logra esto a través de métodos especiales (dunder).
Puedes hacer que tus propias clases respondan a operadores definiendo estos métodos:
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
return Vector(self.x + other.x, self.y + other.y)
def __mul__(self, scalar):
return Vector(self.x * scalar, self.y * scalar)
def __repr__(self):
return f"Vector({self.x}, {self.y})"
v1 = Vector(1, 2)
v2 = Vector(3, 4)
print(v1 + v2) # Vector(4, 6)
print(v1 * 3) # Vector(3, 6)El mismo operador + ahora se comporta de manera diferente según si los operandos son enteros, strings o objetos Vector. Esto es polimorfismo a nivel de operador.
Para una revisión profunda de los métodos especiales de Python, consulta Python Magic Methods.
Polimorfismo con clases base abstractas
Las clases base abstractas (ABCs) van un paso más allá del duck typing al aplicar la interfaz en el momento de la definición de la clase. Si una subclase no implementa un método requerido, Python lanza un TypeError en el momento en que intentas instanciarla.
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self) -> float:
"""Return the area of the shape."""
@abstractmethod
def perimeter(self) -> float:
"""Return the perimeter of the shape."""
class Circle(Shape):
def __init__(self, radius: float):
self.radius = radius
def area(self) -> float:
import math
return math.pi * self.radius ** 2
def perimeter(self) -> float:
import math
return 2 * math.pi * self.radius
class Square(Shape):
def __init__(self, side: float):
self.side = side
def area(self) -> float:
return self.side ** 2
def perimeter(self) -> float:
return 4 * self.side
def print_info(shape: Shape) -> None:
print(f"Area: {shape.area():.2f}")
print(f"Perimeter: {shape.perimeter():.2f}")
print_info(Circle(5))
# Area: 78.54
# Perimeter: 31.42
print_info(Square(4))
# Area: 16.00
# Perimeter: 16.00Si intentas instanciar una subclase que no implementa area():
class Blob(Shape):
pass # forgot to implement area() and perimeter()
b = Blob()
# TypeError: Can't instantiate abstract class Blob with abstract methods area, perimeterLas ABCs te dan la red de seguridad de un contrato formal y al mismo tiempo permiten el polimorfismo en tiempo de ejecución.
ABCs versus duck typing — ¿cuál deberías usar?
- El duck typing es más simple y flexible. Prefierelo para bases de código internas pequeñas y scripts.
- Las ABCs hacen explícita la interfaz esperada, detectan errores de métodos faltantes con anticipación y aparecen en el autocompletado de IDE y en los verificadores de tipos. Prefierelas en bases de código más grandes, bibliotecas públicas y en cualquier lugar donde quieras aplicar un contrato.
Un ejemplo del mundo real: procesamiento de pagos
El polimorfismo brilla en los diseños estilo plugin. Considera un sistema de pagos que debe admitir múltiples proveedores:
from abc import ABC, abstractmethod
class PaymentProvider(ABC):
@abstractmethod
def charge(self, amount: float, currency: str) -> bool:
"""Attempt to charge the given amount. Return True on success."""
@abstractmethod
def refund(self, transaction_id: str) -> bool:
"""Refund a previous transaction. Return True on success."""
class StripeProvider(PaymentProvider):
def charge(self, amount: float, currency: str) -> bool:
print(f"[Stripe] Charged {amount} {currency}")
return True
def refund(self, transaction_id: str) -> bool:
print(f"[Stripe] Refunded transaction {transaction_id}")
return True
class PayPalProvider(PaymentProvider):
def charge(self, amount: float, currency: str) -> bool:
print(f"[PayPal] Charged {amount} {currency}")
return True
def refund(self, transaction_id: str) -> bool:
print(f"[PayPal] Refunded transaction {transaction_id}")
return True
def process_order(provider: PaymentProvider, amount: float) -> None:
success = provider.charge(amount, "USD")
if success:
print("Order complete.")
process_order(StripeProvider(), 99.99)
# [Stripe] Charged 99.99 USD
# Order complete.
process_order(PayPalProvider(), 49.5)
# [PayPal] Charged 49.5 USD
# Order complete.process_order no sabe ni le importa si recibe un StripeProvider o un PayPalProvider. Agregar un nuevo CryptoProvider solo requiere escribir la nueva clase — nada más cambia. Esto es el polimorfismo brindando extensibilidad en el mundo real.
Errores comunes
Error 1: Verificar tipos con type() en lugar de isinstance()
Comparar type(obj) == Dog anula el polimorfismo porque devuelve False para las subclases. Prefiere isinstance(obj, Animal), que devuelve True tanto para Dog como para cualquier subclase futura:
class Animal:
pass
class Dog(Animal):
pass
d = Dog()
# Fragile — breaks for subclasses:
print(type(d) == Animal) # False
# Correct — subclass-aware:
print(isinstance(d, Animal)) # TrueError 2: Firmas de métodos inconsistentes
El polimorfismo supone que todas las implementaciones de un método aceptan los mismos argumentos. Si Dog.speak() requiere un argumento que Cat.speak() no tiene, los llamadores que los tratan de forma uniforme fallarán:
# Inconsistent — will cause errors in a loop
class Dog:
def speak(self, volume): # extra argument!
return f"Woof at volume {volume}"
class Cat:
def speak(self):
return "Meow!"Mantén las firmas de los métodos consistentes entre las clases polimórficas.
Error 3: Argumentos por defecto mutables en métodos sobrescritos
Este es un error más amplio de Python, pero confunde a la gente en las jerarquías de clases: nunca uses un valor mutable por defecto (lista, dict) como valor de argumento predeterminado — se crea una sola vez y se comparte entre todas las llamadas.
# Bug: the list is shared across all instances
class Item:
def __init__(self, tags=[]): # BAD
self.tags = tags
# Fix:
class Item:
def __init__(self, tags=None):
self.tags = tags if tags is not None else []Resumen
| Concepto | Qué significa |
|---|---|
| Sobrescritura de métodos | Una subclase proporciona su propia versión de un método padre |
| Despacho dinámico | Python elige la versión correcta del método en tiempo de ejecución |
| Duck typing | Cualquier object con los métodos correctos funciona, independientemente de su clase |
| Sobrecarga de operadores | Los métodos dunder (__add__, __len__, …) hacen que los operadores sean polimórficos |
| Clases base abstractas | Aplican formalmente la interfaz que las subclases deben implementar |
El polimorfismo es lo que hace que el código sea extensible sin modificación. Escribe funciones que dependan del comportamiento (nombres de métodos), no de tipos concretos, y tu código acomodará naturalmente nuevas clases sin cambios.