DMarlon Studio

Lección 14 · 14 de 20

Anotaciones de tipo, protocolos y genéricos · Python

Anotaciones de tipo en Python: alias con type, typing.Protocol para el tipado estructural y funciones genéricas con la sintaxis de Python 3.12.

Python · Anotaciones de tipo, protocolos y genéricos en DMarlon Studio: el código del ejemplo y su salida en la terminal

Las anotaciones de tipo (title: str, -> Euros) documentan qué espera y qué devuelve cada función. Python no las comprueba al ejecutar; las usan los editores y herramientas como mypy o Pyright para avisar de errores antes de ejecutar el programa. type Euros = float crea un alias: un nombre más expresivo para un tipo que ya existe.

Un `Protocol` describe una forma, no una familia: Priced dice que vale cualquier objeto con un método price() que devuelva euros. Book y Subscription no heredan de Priced y aun así lo cumplen. Es el tipado estructural (el *duck typing* de siempre, pero comprobable): si tiene price(), sirve.

Por eso basket_total(items: list[Priced], ...) acepta libros, suscripciones y cualquier clase futura que tenga price(). Con `@runtime_checkable`, además, isinstance(basket[1], Priced) se puede preguntar al ejecutar: la suscripción lo cumple y la cadena "Dune" no.

def first[T](items: list[T], default: T) -> T es una función genérica (sintaxis de Python 3.12; antes se usaba TypeVar). T es un tipo cualquiera, pero el mismo en toda la firma: con una lista de cadenas, el verificador sabe que el resultado es una cadena.

La última línea lo deja claro: Subscription("3") pasa una cadena donde se esperaba un int y Python lo ejecuta sin quejarse. Un verificador de tipos lo marcaría como error.

Código · main.py

from typing import Protocol, runtime_checkabletype Euros = float                    # a type alias (Python 3.12)@runtime_checkableclass Priced(Protocol):               # any class with price() fits    def price(self) -> Euros: ...class Book:                           # no inheritance from Priced needed    def __init__(self, title: str, cost: Euros) -> None:        self.title = title        self.cost = cost    def price(self) -> Euros:        return self.costclass Subscription:    def __init__(self, months: int) -> None:        self.months = months    def price(self) -> Euros:        return self.months * 7.99def basket_total(items: list[Priced], vat: float = 0.04) -> Euros:    return round(sum(item.price() for item in items) * (1 + vat), 2)def first[T](items: list[T], default: T) -> T:    # a generic function    return items[0] if items else defaultbasket: list[Priced] = [Book("Dune", 11.5), Subscription(3)]print("Total:", basket_total(basket))print("Is Priced:", isinstance(basket[1], Priced), isinstance("Dune", Priced))print("First:", first(["INV-1", "INV-2"], "none"), "|", first([], 0))print("Not enforced:", repr(Subscription("3").months))   # runs anyway

Salida · python3 main.py

Total: 36.89
Is Priced: True False
First: INV-1 | 0
Not enforced: '3'

Para recordar

  • Las anotaciones no cambian lo que hace el programa: lo documentan.
  • type Nombre = ... crea un alias de tipo.
  • Un Protocol acepta cualquier clase con los métodos indicados.
  • def f[T](x: T) -> T declara una función genérica.
  • Use mypy o Pyright para que las anotaciones detecten errores.

Ejercicio

Cree una clase GiftCard con price() y añádala a basket sin heredar de nada; después escriba def last[T](items: list[T], default: T) -> T.