Objetivo de aprendizaje
Al concluir este tema, el estudiante será capaz de:
- Distinguir los tres tipos de polimorfismo (de subtipo, ad hoc y paramétrico) según cómo se elige la implementación.
- Explicar el tipado estructural (duck typing) de Python y compararlo con el tipado nominal de Java y C++.
- Diseñar contratos con clases abstractas e interfaces, y predecir en qué momento se detecta su incumplimiento.
- Comparar los genéricos de Java, las plantillas de C++ y el tipado dinámico de Python respecto a cuándo se verifican los tipos.
- Decidir entre herencia y composición, y aplicar métodos especiales y
dataclass.
Ruta de trabajo del tema
| Etapa | Contenido | Producto del estudiante |
|---|---|---|
| 1 | Planteamiento inicial | Hipótesis sobre qué exige una función a sus argumentos |
| 2 | Tres formas de polimorfismo | Clasificación por mecanismo de elección |
| 3 | Polimorfismo ad hoc: sobrecarga y operadores | Predicción de la resolución |
| 4 | Duck typing: tipado estructural | Regla propia sobre errores y momento de detección |
| 5 | Contratos: interfaces y clases abstractas | Diseño de un contrato |
| 6 | Genéricos y plantillas | Clasificación de casos en tres lenguajes |
| 7 | Composición frente a herencia; dataclass |
Criterio de elección |
| 8 | Verificación y reformulación | Cuestionario y respuesta revisada |
1. Planteamiento inicial
La biblioteca ahora presta libros, podcasts y otros recursos que no comparten una clase base. Un programador escribe una función que muestra cualquier recurso y la prueba con tres objetos.
class Libro:
def describir(self):
return "Libro de papel"
class Podcast:
def describir(self):
return "Audio en linea"
class Mueble:
pass
def mostrar(x):
print(x.describir())
mostrar(Libro())
mostrar(Podcast())
mostrar(Mueble())
Este laboratorio interactivo requiere JavaScript habilitado en el navegador.
Esta hipótesis se retomará en la sección 4.
2. Tres formas de polimorfismo
Definición — Polimorfismo: capacidad de una operación de comportarse de varias maneras. Se materializa de tres formas, que se distinguen por cómo se elige la implementación.
| Tipo | Nombre habitual | Cómo se elige la implementación | Ejemplo |
|---|---|---|---|
| De subtipo | Sobrescritura, despacho dinámico | Por el tipo real del objeto, en ejecución | material.plazo() |
| Ad hoc | Sobrecarga (de funciones y de operadores) | Por la firma de la llamada (número y tipos de los argumentos), en general al compilar | sumar(int, int) y sumar(double, double); + |
| Paramétrico | Genéricos, plantillas | Por el tipo con el que se instancia un código único | Caja<T>, template <typename T> |
El de subtipo se estudió en el tema 3.1. Esta es una mirada rápida a los tres en un programa de Python:
Este laboratorio interactivo requiere JavaScript habilitado en el navegador.
Clasifique ahora los siguientes ejemplos (Java, C++ y Python). Los fragmentos de Java y C++ son afirmaciones didácticas; no se ejecutan en esta página.
Este laboratorio interactivo requiere JavaScript habilitado en el navegador.
3. Polimorfismo ad hoc: sobrecarga y operadores
Sobrecarga de funciones
En Java y C++ pueden coexistir varias funciones con el mismo nombre y distintos parámetros; el compilador elige según los argumentos (la resolución simplificada se estudió en el tema 2.4). Python no lo hace: un nombre se liga a un objeto, y definir otra función con el mismo nombre religa el nombre.
Este laboratorio interactivo requiere JavaScript habilitado en el navegador.
Sobrecarga de operadores: métodos especiales
En Python, los operadores y varias funciones incorporadas delegan en métodos especiales (los que empiezan y terminan con doble guion bajo). Si una clase los define, sus objetos participan en esas operaciones.
| Operación | Método especial | Ejemplo de uso |
|---|---|---|
a + b |
__add__ |
Sumar importes |
a == b |
__eq__ |
Comparar por contenido en lugar de por identidad |
a < b |
__lt__ |
Permitir sorted |
str(a), print(a) |
__str__ |
Texto para el usuario |
repr(a) |
__repr__ |
Texto para el programador |
len(a) |
__len__ |
Tamaño de una colección propia |
class Dinero:
def __init__(self, pesos):
self.pesos = pesos
def __add__(self, otro): # define el significado de "a + b"
return Dinero(self.pesos + otro.pesos)
def __eq__(self, otro): # define el significado de "a == b"
return self.pesos == otro.pesos
def __lt__(self, otro): # define el significado de "a < b"
return self.pesos < otro.pesos
def __str__(self): # lo que muestra print
return "$" + str(self.pesos)
a = Dinero(5)
b = Dinero(5)
print(a + b, a == b, a is b, a < Dinero(9))
print(sorted([Dinero(30), Dinero(10), Dinero(20)])[0])
Este laboratorio interactivo requiere JavaScript habilitado en el navegador.
Este laboratorio interactivo requiere JavaScript habilitado en el navegador.
Error común: pensar que
==compara siempre el contenido. En una clase propia, sin__eq__, compara identidad.
4. Duck typing: tipado estructural
Definición — Duck typing (tipado estructural): en Python, lo que importa de un objeto no es su clase, sino lo que sabe hacer: si responde a los mensajes que el código le envía, funciona. Se resume en la frase «si camina como un pato y suena como un pato, es un pato».
En el planteamiento inicial, Libro y Podcast no comparten ninguna clase base y aun así funcionan con mostrar, porque ambos tienen un método describir. Mueble no lo tiene, y el error aparece cuando se ejecuta la llamada, no antes.
Este laboratorio interactivo requiere JavaScript habilitado en el navegador.
Este laboratorio interactivo requiere JavaScript habilitado en el navegador.
| Aspecto | Tipado estructural (Python, en ejecución) | Tipado nominal (Java, C++) |
|---|---|---|
| Qué se pide al objeto | Que tenga los métodos necesarios | Que pertenezca a una clase o implemente una interfaz declarada |
| Cuándo se detecta un incumplimiento | En ejecución, al llamar al método | Al compilar |
| Ventaja | Flexibilidad: sirve cualquier objeto compatible | Seguridad: los errores se detectan antes de ejecutar |
| Riesgo | Un error puede aparecer tarde, en producción | Más declaraciones y jerarquías |
Nota: C++ ofrece un caso intermedio: las plantillas se comprueban de manera estructural, pero al compilar. Una función
template <typename T> void mostrar(T x) { x.describir(); }acepta cualquier tipo que tengadescribir, y el error de un tipo que no la tenga aparece al compilar.
5. Contratos: interfaces y clases abstractas
Definición — Contrato: conjunto de operaciones que una clase promete ofrecer, sin dar su implementación. Una interfaz es un contrato puro. Una clase abstracta es un contrato que, además, puede aportar estado y comportamiento común; no puede instanciarse por sí sola.
El contrato resuelve un problema del duck typing: hacer explícita la promesa. Quien lee la clase Prestable sabe qué debe ofrecer cualquier material prestable; quien escribe una subclase sabe qué debe implementar. Python ofrece tres niveles de contrato, de menos a más estricto:
| Nivel | Mecanismo | El incumplimiento se detecta… |
|---|---|---|
| Convención | Documentar la interfaz esperada (duck typing) | Al llamar al método inexistente |
| Contrato por excepción | El método de la base lanza NotImplementedError |
Al llamar al método sin implementar |
| Contrato formal | ABC y @abstractmethod |
Al crear el objeto |
from abc import ABC, abstractmethod
class Prestable(ABC): # clase abstracta: define un contrato
@abstractmethod
def plazo(self): # operación que TODA subclase debe implementar
pass
def resumen(self): # comportamiento común, ya implementado
return "Plazo de " + str(self.plazo()) + " dias"
class Libro(Prestable):
def plazo(self): # cumple el contrato
return 7
class Incompleto(Prestable): # NO implementa plazo
pass
print(Libro().resumen())
x = Incompleto() # error: no se puede crear
Este laboratorio interactivo requiere JavaScript habilitado en el navegador.
Este laboratorio interactivo requiere JavaScript habilitado en el navegador.
Compare con el contrato «por excepción», sin clases abstractas formales:
Este laboratorio interactivo requiere JavaScript habilitado en el navegador.
Interfaces y clases abstractas en Java y C++
Los fragmentos siguientes comparan la misma idea. No se ejecutan en esta página.
// Java: interface = contrato puro. Una clase puede implementar varias.
interface Prestable {
int plazo(); // sin implementación
default String resumen() { // comportamiento común (desde Java 8)
return "Plazo de " + plazo() + " dias";
}
}
class Libro implements Prestable {
public int plazo() { return 7; }
}
// Prestable p = new Prestable(); // error de compilación: es una interfaz
// C++: una clase con un método virtual puro (= 0) es abstracta.
class Prestable {
public:
virtual int plazo() const = 0; // contrato: sin implementación
virtual ~Prestable() = default;
};
class Libro : public Prestable {
public:
int plazo() const override { return 7; }
};
// Prestable p; // error de compilación: clase abstracta
| Aspecto | Python | Java | C++ |
|---|---|---|---|
| Contrato puro | Clase con métodos @abstractmethod |
interface |
Clase solo con métodos virtuales puros |
| Contrato con comportamiento común | Clase ABC con métodos concretos |
abstract class, o métodos default en interfaces |
Clase con virtuales puros y métodos normales |
| Se detecta el incumplimiento | Al crear el objeto (con ABC) o al llamar | Al compilar | Al compilar |
| Herencia de varios contratos | Sí (herencia múltiple) | Sí (varias interfaces; una sola clase base) | Sí (herencia múltiple) |
Importante: la diferencia práctica entre «interfaz» y «clase abstracta» depende del lenguaje. En Java, las interfaces no tienen estado propio y una clase solo hereda de una clase base. En Python y C++ ambas ideas se expresan con clases.
6. Genéricos y plantillas: polimorfismo paramétrico
Definición — Polimorfismo paramétrico: escribir una sola vez un algoritmo o una estructura de datos que funciona para cualquier tipo, y fijar el tipo al usarlo. Se llama «paramétrico» porque el tipo es un parámetro:
Caja<T>.
Sin genéricos habría que duplicar el código por cada tipo (una CajaDeEnteros, una CajaDeTextos) o perder la verificación de tipos (una Caja de Object). Los genéricos dan lo mejor de ambos mundos: un solo código y tipos verificados.
// Java: genéricos. T es un parámetro de tipo.
class Caja<T> {
private T valor;
Caja(T valor) { this.valor = valor; }
T get() { return valor; }
}
Caja<Integer> c = new Caja<>(5);
int n = c.get(); // el compilador sabe que es Integer
// Caja<String> s = new Caja<>(5); // error de compilación
// C++: plantillas. El compilador genera una versión por cada tipo que se usa.
template <typename T>
class Caja {
T valor;
public:
Caja(T v) : valor(v) {}
T get() const { return valor; }
};
Caja<int> c(5);
# Python: no necesita genéricos para funcionar con cualquier tipo (tipado dinámico).
# Las anotaciones documentan la intención y las herramientas externas (como mypy) las verifican,
# pero el intérprete NO las verifica al ejecutar.
from typing import Generic, TypeVar
T = TypeVar("T")
class Caja(Generic[T]):
def __init__(self, valor: T):
self.valor = valor
def get(self) -> T:
return self.valor
| Aspecto | Java | C++ | Python |
|---|---|---|---|
| Mecanismo | Genéricos | Plantillas | Tipado dinámico (anotaciones opcionales) |
| Cuándo se verifican los tipos | Al compilar | Al compilar (en cada instanciación) | En ejecución (las anotaciones, solo con herramientas externas) |
| Qué se genera | Un solo código; el parámetro de tipo se borra (type erasure) | Una versión por cada tipo | Nada: el mismo código atiende todo |
| Tipos primitivos como parámetro | No (se usa Integer) |
Sí | No aplica |
Determine qué ocurre con cada fragmento. Los fragmentos de Java, C++ y Python son afirmaciones didácticas: no se ejecutan en esta página.
Este laboratorio interactivo requiere JavaScript habilitado en el navegador.
Importante: estos fragmentos y sus resultados son afirmaciones sobre los lenguajes, no resultados ejecutados por esta página. Verifíquelos con un compilador o un intérprete si desea mostrarlos en clase.
7. Composición frente a herencia; dataclass
Composición
Definición — Composición: una clase usa otra guardando un objeto suyo como atributo y delegándole trabajo. Expresa una relación «tiene un».
Este laboratorio interactivo requiere JavaScript habilitado en el navegador.
La regla práctica: herencia cuando «es un» es verdadero y se necesita sustituir un objeto por otro; composición cuando solo se necesita usar sus servicios. La composición es menos rígida: se puede cambiar el objeto contenido sin alterar la jerarquía.
Definición — Principio de sustitución de Liskov: un objeto de una subclase debe poder usarse en cualquier lugar donde se espere uno de su superclase sin que el programa se comporte incorrectamente. Un ejemplo clásico de violación: hacer que un cuadrado herede de un rectángulo. En matemáticas, un cuadrado es un rectángulo; con objetos mutables, un cuadrado no puede permitir que se cambie su ancho sin cambiar su alto, y el código escrito para rectángulos deja de funcionar.
dataclass
Muchas clases solo guardan datos. Escribir a mano __init__, __repr__ y __eq__ para cada una es repetitivo. El decorador @dataclass los genera a partir de los campos anotados.
from dataclasses import dataclass
@dataclass
class Libro:
codigo: int # campo obligatorio
titulo: str # campo obligatorio
disponible: bool = True # campo con valor por omisión
a = Libro(101, "Fisica I")
b = Libro(101, "Fisica I")
print(a) # __repr__ generado
print(a == b, a is b) # __eq__ generado: compara campos; la identidad es distinta
b.disponible = False
print(b)
Este laboratorio interactivo requiere JavaScript habilitado en el navegador.
Este laboratorio interactivo requiere JavaScript habilitado en el navegador.
Importante: un dataclass no admite un valor por omisión mutable (por ejemplo,
lista: list = []): todos los objetos compartirían la misma lista, el mismo problema que el atributo de clase mutable del tema 3.2. Python lo rechaza con un error.
Excepciones propias: polimorfismo al manejar errores
Las excepciones también forman jerarquías. Un manejador except de la clase base captura a todas las subclases: es polimorfismo de subtipo aplicado al manejo de errores.
Este laboratorio interactivo requiere JavaScript habilitado en el navegador.
8. Errores conceptuales frecuentes
Error común: pensar que el polimorfismo siempre requiere herencia. El duck typing y los genéricos lo logran sin ella.
Error común: esperar sobrecarga por firma en Python. El nombre se liga a un solo objeto; el último
defgana.
Error común: creer que una anotación de tipo en Python impide pasar un tipo distinto. Las anotaciones no se verifican al ejecutar.
Error común: crear una jerarquía de herencia solo para reutilizar código. Si «es un» no es verdadero, se debe usar composición.
Error común: confundir interfaz con implementación y esperar que una interfaz de Java guarde estado.
Este laboratorio interactivo requiere JavaScript habilitado en el navegador.
9. Libreta de trabajo
Este laboratorio interactivo requiere JavaScript habilitado en el navegador.
10. Preguntas de reflexión
- ¿Qué ventajas tiene detectar un incumplimiento del contrato al compilar frente a detectarlo al ejecutar? Responda desde el punto de vista de quien programa y de quien mantiene un sistema grande.
- Un equipo discute si un
Cuadradodebe heredar deRectangulo. Argumente a favor y en contra con el principio de sustitución. - Describa un caso en el que preferiría duck typing y otro en el que preferiría un contrato formal con
ABC. - ¿Por qué Python puede prescindir de los genéricos para funcionar con cualquier tipo? ¿Qué se pierde con ello?
- Elija una clase de su dominio (de la biblioteca, el estacionamiento u otro) y diseñe un contrato (
ABC) con dos operaciones y dos implementaciones.
11. Lecturas recomendadas
- Sebesta, R. (2015). Concepts of programming languages (11th ed.), capítulos sobre soporte para la programación orientada a objetos y sobre tipos de datos abstractos y construcciones de encapsulamiento. Pearson.
- Cardelli, L., & Wegner, P. (1985). On understanding types, data abstraction, and polymorphism. ACM Computing Surveys, 17(4), 471–522. (Clasificación clásica de las formas de polimorfismo.)
- Liskov, B., & Wing, J. (1994). A behavioral notion of subtyping. ACM Transactions on Programming Languages and Systems, 16(6), 1811–1841.
- Python Software Foundation. abc — Abstract Base Classes y dataclasses — Data Classes, en la biblioteca estándar. Acceso libre en docs.python.org.
- Python Software Foundation. The Python Language Reference, sección «Data model» (métodos especiales). Acceso libre en docs.python.org.