5.D Jerarquías cerradas y clases de datos¶
El problema: un conjunto de variantes conocido y cerrado¶
A veces los distintos tipos no se van a ampliar: un pago es en efectivo, con tarjeta o por Bizum, y no hay una cuarta posibilidad que inventar desde fuera. Con una jerarquía normal, quien use el tipo base nunca está seguro de haber cubierto todos los casos.
Una jerarquía cerrada (sealed) le dice al compilador exactamente cuáles son los subtipos posibles. Así, cuando se hace algo distinto según el subtipo, el compilador comprueba que no falte ninguno: si más adelante se añade un caso nuevo, avisa en todos los sitios donde falta tratarlo.
Python no tiene sealed. Se emula con clases de datos (@dataclass) y un tipo unión (Pago = Efectivo | Tarjeta | Bizum), y se consulta con match, que reconoce cada clase y extrae sus campos. Python no comprueba en ejecución que se hayan cubierto todos los casos; esa comprobación la hacen verificadores de tipos como mypy o pyright.
from dataclasses import dataclass
@dataclass(frozen=True)
class Efectivo:
importe: int
@dataclass(frozen=True)
class Tarjeta:
importe: int
ultimos4: str
@dataclass(frozen=True)
class Bizum:
importe: int
telefono: str
Pago = Efectivo | Tarjeta | Bizum
def describir(pago: Pago) -> str:
match pago:
case Efectivo(importe=importe):
return f"Efectivo de {importe} €"
case Tarjeta(importe=importe, ultimos4=ultimos4):
return f"Tarjeta ****{ultimos4} de {importe} €"
case Bizum(importe=importe, telefono=telefono):
return f"Bizum al {telefono} de {importe} €"
def comision(pago: Pago) -> int:
match pago:
case Efectivo():
return 0
case Tarjeta(importe=importe):
return importe * 2 // 100
case Bizum():
return 1
if __name__ == "__main__":
pagos = [Efectivo(50), Tarjeta(100, "1234"), Bizum(30, "600123456")]
total = 0
for pago in pagos:
c = comision(pago)
total += c
print(f"{describir(pago)} -> comisión {c} €")
print(f"total de comisiones: {total} €")
Salida:
Efectivo de 50 € -> comisión 0 €
Tarjeta ****1234 de 100 € -> comisión 2 €
Bizum al 600123456 de 30 € -> comisión 1 €
total de comisiones: 3 €
Observa:
- Cada variante lleva datos distintos (
ultimos4solo existe en la tarjeta,telefonosolo en Bizum). Por eso aquí no vale un enumerado. - Las variantes son clases de datos, que solo guardan información y comparan por valor (ver 4.B).
- Las funciones
describirycomisionno tienen caso por defecto: no hace falta, porque están cubiertos todos los posibles. - El comportamiento está fuera de las clases (en las funciones), no dentro de ellas. Se elige así cuando las variantes son solo datos y las operaciones se van añadiendo.
¿Enumerado, jerarquía cerrada, abstracta o interfaz?¶
| Situación | Usa |
|---|---|
| Valores fijos y sencillos, a lo sumo con datos iguales para todos (días, tamaños, estados) | Enumerado |
| Variantes cerradas, cada una con datos distintos (pagos, resultados, formas de un mensaje) | Jerarquía cerrada |
| Familia abierta con código común | Clase abstracta |
| Capacidad que clases distintas pueden ofrecer | Interfaz |
Errores frecuentes¶
| Error | Cómo evitarlo |
|---|---|
Poner un caso default/else que esconde los casos nuevos |
No lo pongas: así el compilador te avisa si falta uno |
| Usar una jerarquía cerrada cuando otras personas deben poder añadir variantes | Interfaz o clase abstracta (jerarquía abierta) |
Meter la lógica de cada caso en una cadena de if y instanceof |
Un switch/when/match con patrones |
| Usar un enumerado cuando cada variante necesita datos propios | Jerarquía cerrada |
Para practicar¶
El ejercicio de la biblioteca de U5.1 usa clases de datos y una jerarquía cerrada para los usuarios. La comparación entre lenguajes de todos estos tipos de clase está en Pasar de uno a otro.