Saltar a contenido

A2.A Tipos parametrizados

Para quién es esto

Es material avanzado: da por sabidas las unidades 1 a 5, sobre todo clases e interfaces. Si te haces un lío con los tipos, repasa las unidades 4 y 5.

Una clase genérica o una función genérica se escribe una sola vez y sirve para varios tipos. En lugar de decir «una caja de enteros» y «una caja de textos» con dos clases, dices «una caja de T», donde T es un tipo que se decide al usarla.

Por qué existe

Python no obliga a nada: una lista admite cualquier cosa y una función acepta cualquier argumento. Las anotaciones genéricas sirven para documentar y para que un comprobador (mypy, pyright o tu editor) avise de errores antes de ejecutar, aunque el intérprete las ignora.

La sintaxis en Python

Qué Cómo se escribe en Python
Declarar el tipo variable T = TypeVar("T") (de typing)
Clase genérica class Caja(Generic[T]): ...
Función genérica def primero(lista: list[T]) -> T:
Usarla numero: Caja[int] = Caja(42) (anotación opcional)
Varios parámetros class Par(Generic[A, B])
Sintaxis nueva (3.12+) class Caja[T]: y def primero[T](lista: list[T]) -> T:, sin TypeVar

Por convención, el parámetro de tipo se llama con una letra mayúscula: T (type), E (element), K y V (key y value), o A, B cuando hay varios.

Un ejemplo

Una caja genérica, un par con dos tipos distintos y una función que devuelve el primer elemento de una lista de cualquier tipo:

# Tipos genéricos: Caja[T] guarda un valor de cualquier tipo T.
# En Python los tipos son ANOTACIONES: las lee un comprobador como mypy o el editor, no las exige el intérprete.
from typing import Generic, TypeVar

T = TypeVar("T")
A = TypeVar("A")
B = TypeVar("B")


class Caja(Generic[T]):
    def __init__(self, valor: T):
        self.valor = valor


# Con dos parámetros de tipo: Par[A, B]
class Par(Generic[A, B]):
    def __init__(self, primero: A, segundo: B):
        self.primero = primero
        self.segundo = segundo

    def __str__(self):
        return f"({self.primero}, {self.segundo})"


# Función genérica: sirve para listas de cualquier tipo y devuelve ese mismo tipo
def primero(lista: list[T]) -> T:
    return lista[0]


texto = Caja("hola")  # un comprobador de tipos deduce Caja[str]
numero: Caja[int] = Caja(42)  # también se puede anotar a mano
print(f"caja de texto: {texto.valor}")
print(f"caja de entero: {numero.valor}")
print(f"par: {Par('Ana', 30)}")
print(f"primero de [3, 4, 5]: {primero([3, 4, 5])}")
print(f"primero de [a, b]: {primero(['a', 'b'])}")

# mypy avisaría de: n: int = texto.valor (es un str), pero el programa se ejecutaría igualmente
t: str = texto.valor
print(f"longitud: {len(t)}")

Salida:

caja de texto: hola
caja de entero: 42
par: (Ana, 30)
primero de [3, 4, 5]: 3
primero de [a, b]: a
longitud: 4

Fíjate en lo que no hace falta: ni convertir tipos ni escribir una clase por cada tipo. La función primero devuelve un entero cuando le das enteros y un texto cuando le das textos, y el lenguaje lo sabe.

Errores frecuentes

Error Cómo evitarlo
Usar Object/Any/dynamic «para que valga con todo» Si lo único que cambia es el tipo, usa un parámetro de tipo
Escribir T y luego querer llamar a un método suyo Sin restricción, de T no se sabe nada: mira A2.B
Creer que un genérico existe igual al ejecutar en todos los lenguajes No es así: mira A2.C

Para practicar

Los ejercicios A2.1 y A2.2 usan una función y una clase genéricas. Para ver la misma idea en los otros lenguajes: Pasar de uno a otro.