Unidad III · Tema 3.3 Disponible

Polimorfismo, genéricos y contratos

Polimorfismo de subtipo, ad hoc y paramétrico; interfaces y clases abstractas como contratos.

Objetivo de aprendizaje

Al concluir este tema, el estudiante será capaz de:

  1. Distinguir los tres tipos de polimorfismo (de subtipo, ad hoc y paramétrico) según cómo se elige la implementación.
  2. Explicar el tipado estructural (duck typing) de Python y compararlo con el tipado nominal de Java y C++.
  3. Diseñar contratos con clases abstractas e interfaces, y predecir en qué momento se detecta su incumplimiento.
  4. 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.
  5. 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 tenga describir, 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 def gana.

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

  1. ¿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.
  2. Un equipo discute si un Cuadrado debe heredar de Rectangulo. Argumente a favor y en contra con el principio de sustitución.
  3. Describa un caso en el que preferiría duck typing y otro en el que preferiría un contrato formal con ABC.
  4. ¿Por qué Python puede prescindir de los genéricos para funcionar con cualquier tipo? ¿Qué se pierde con ello?
  5. 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.