Lección 11 · 11 de 20
Clases de datos · Python
Clases de datos en Python con @dataclass: __init__, __repr__ y __eq__ automáticos, field(default_factory), __post_init__, frozen y order.

Muchas clases solo guardan datos. Con el decorador `@dataclass` basta con declarar los campos con su tipo (product: str, quantity: int…) y Python escribe por usted __init__, __repr__ y __eq__. Por eso print(inv.lines[0]) sale legible y dos Line con los mismos datos son iguales con ==.
Los campos con valor por defecto van al final: paid: bool = False. Para una lista no vale lines: list = [], porque todas las facturas compartirían la misma; `field(default_factory=list)` crea una lista nueva para cada objeto.
`__post_init__` se ejecuta justo después del __init__ generado y sirve para validar o limpiar datos: aquí quita los espacios del nombre del cliente y lo pone con mayúsculas iniciales. Las clases de datos admiten métodos normales, como total().
@dataclass(frozen=True, order=True) añade dos cosas a Version: con `frozen` los objetos no se pueden modificar (asignar un campo lanza un error) y con `order` se comparan campo a campo, como tuplas. Así max() sabe que la 1.10 es más nueva que la 1.9, cosa que una comparación de textos haría mal.
asdict(inv) convierte la factura, con sus líneas, en diccionarios normales: es el paso previo habitual para guardarla en JSON.
Código · main.py
from dataclasses import dataclass, field, asdict@dataclassclass Line: product: str quantity: int unit_price: float@dataclassclass Invoice: number: str customer: str lines: list[Line] = field(default_factory=list) # a new list each time paid: bool = False def __post_init__(self): # runs right after the generated __init__ self.customer = self.customer.strip().title() def total(self): return sum(l.quantity * l.unit_price for l in self.lines)@dataclass(frozen=True, order=True) # immutable and comparableclass Version: major: int minor: intinv = Invoice("F-2026-014", " maria lopez ")inv.lines.append(Line("Chair", 4, 49.5))inv.lines.append(Line("Table", 1, 230.0))print(inv.lines[0]) # free __repr__print(inv.customer, "owes", inv.total(), "| paid:", inv.paid)print("Equal:", Line("Chair", 4, 49.5) == inv.lines[0]) # free __eq__print(asdict(inv)["lines"][1])print("Newest:", max(Version(1, 9), Version(1, 10), Version(0, 99)))Salida · python3 main.py
Line(product='Chair', quantity=4, unit_price=49.5)
Maria Lopez owes 428.0 | paid: False
Equal: True
{'product': 'Table', 'quantity': 1, 'unit_price': 230.0}
Newest: Version(major=1, minor=10)Para recordar
@dataclassgenera__init__,__repr__y__eq__.- Use
field(default_factory=list)para listas y diccionarios por defecto. __post_init__valida o ajusta los datos al crear el objeto.frozen=Truela hace inmutable;order=True, comparable.asdict()la convierte en un diccionario.
Ejercicio
Añada a Line un campo discount: float = 0.0 y haga que Invoice.total() lo tenga en cuenta; ponga un 10 % de descuento a las sillas.