La lliçó anterior va acabar amb una confessió: entre __init__, __repr__ i __eq__, la classe Llibre dedica la meitat de les seves línies a recitar els seus propis atributs — titol, preu, estoc — una vegada i una altra. Aquest codi repetitiu (anomenat boilerplate) és tan universal que Python el va automatitzar a la biblioteca estàndard: el decorador @dataclass genera aquests mètodes per tu a partir d'una simple llista de camps. En aquesta lliçó reescriuràs Llibre en un terç de les línies, coneixeràs field(), frozen=True i order=True, tancaràs la taula de criteri que 04-06 va deixar oberta (namedtuple, dict, classe o dataclass?) i deixaràs el catàleg de Papyrus en la seva forma definitiva: un diccionari d'objectes.

Contingut

  1. El boilerplate que tothom escriu igual
  2. @dataclass: tres mètodes gratis
  3. Valors per defecte i field(default_factory=...)
  4. __post_init__: validar en néixer
  5. frozen=True: dataclasses immutables
  6. order=True: comparables i ordenables
  7. La taula de criteri definitiva: namedtuple vs dict vs classe vs dataclass
  8. El catàleg final de Papyrus: dict d'objectes
  9. Errors comuns i consells
  10. Exercicis amb solucions

El boilerplate que tothom escriu igual

Així va quedar Llibre després de 05-05 (només la part repetitiva):

class Llibre:
    def __init__(self, titol, preu, estoc=0):
        self.titol = titol
        self.preu = preu
        self.estoc = estoc

    def __repr__(self):
        return f"Llibre({self.titol!r}, {self.preu}, {self.estoc})"

    def __eq__(self, altre):
        if not isinstance(altre, Llibre):
            return NotImplemented
        return (self.titol, self.preu, self.estoc) == (altre.titol, altre.preu, altre.estoc)

Quinze línies on cada atribut apareix cinc vegades. Afegir un camp autor obliga a tocar tres mètodes, i oblidar-ne un crea un bug silenciós (un __eq__ que ignora el camp nou). És feina mecànica — just el que les màquines fan millor.

@dataclass: tres mètodes gratis

from dataclasses import dataclass

@dataclass
class Llibre:
    """Un llibre del cataleg de Papyrus."""
    titol: str
    preu: float
    estoc: int = 0

Això és tot. El decorador @dataclass (importat de dataclasses, biblioteca estàndard) llegeix els camps declarats i genera automàticament __init__, __repr__ i __eq__:

odissea = Llibre("L'Odissea", 12.50, 4)      # __init__ generat
print(odissea)             # Llibre(titol="L'Odissea", preu=12.5, estoc=4)  ← __repr__ generat
print(odissea == Llibre("L'Odissea", 12.50, 4))   # True  ← __eq__ generat (camp a camp)
print(odissea == Llibre("L'Odissea", 12.50, 3))   # False (difereix l'estoc)

Dos aclariments importants:

  • Les anotacions titol: str o preu: float són type hints (pistes de tipus), que estudiarem a fons al mòdul 8. De moment en tens prou amb això: @dataclass les necessita per detectar els camps, i no valida tipus en execució — Llibre(42, "hola") no protesta (encara; de seguida arriba __post_init__).
  • Una dataclass és una classe normal i corrent: pot tenir mètodes, properties (05-04), heretar i ser heretada. Només t'estalvia el boilerplate.

Per això li podem afegir el comportament de sempre:

@dataclass
class Llibre:
    titol: str
    preu: float
    estoc: int = 0

    IVA_LLIBRES = 0.04          # sense anotacio de tipus → atribut de classe, no camp
    DESCOMPTE_SOCI = 0.05

    def preu_final(self, soci=False):
        descompte = Llibre.DESCOMPTE_SOCI if soci else 0
        return round(self.preu * (1 - descompte) * (1 + Llibre.IVA_LLIBRES), 2)

    def hi_ha_estoc(self):
        return self.estoc > 0

print(Llibre("Hamlet", 9.95, 6).preu_final(soci=True))   # 9.83 — la xifra canonica

Detall subtil però crucial: IVA_LLIBRES = 0.04 sense anotació és un atribut de classe corrent (05-01) i @dataclass l'ignora com a camp. Amb anotació (IVA_LLIBRES: float = 0.04) es convertiria en un camp més de l'__init__ — gairebé mai el que vols per a una constant.

Valors per defecte i field(default_factory=...)

Els valors per defecte funcionen com a les funcions (estoc: int = 0), i amb la mateixa trampa del mòdul 3: mai un mutable com a valor per defecte. @dataclass és encara més estricte que les funcions — directament ho prohibeix:

@dataclass
class Soci:
    nom: str
    codi: str
    historial: list = []       # ValueError: mutable default <class 'list'> for field historial

La solució és field(default_factory=...): en lloc d'un valor compartit, li dones una funció que fabrica un valor nou per a cada instància:

from dataclasses import dataclass, field

@dataclass
class Soci:
    nom: str
    codi: str
    historial: list = field(default_factory=list)   # una llista NOVA per soci

lluis = Soci("Lluis", "LLUIS-001")
marta = Soci("Marta", "MARTA-002")
lluis.historial.append("L'Odissea")
print(marta.historial)    # [] — cada soci te la seva (sense la trampa de 03-02)

field() admet més ajustos útils: field(compare=False) exclou un camp de l'__eq__/ordre generats, i field(repr=False) l'amaga del __repr__ (útil per a historials llargs).

__post_init__: validar en néixer

Com que __init__ l'escriu el decorador, on posem les validacions de 05-04? A __post_init__, un mètode que la dataclass crida automàticament just després de l'__init__ generat:

@dataclass
class Llibre:
    titol: str
    preu: float
    estoc: int = 0

    def __post_init__(self):
        self.titol = self.titol.strip()
        if not self.titol:
            raise ValueError("El titol no pot estar buit")
        if self.preu < 0:
            raise ValueError(f"No es permet un preu negatiu: {self.preu}")
        if not isinstance(self.estoc, int) or self.estoc < 0:
            raise ValueError(f"Estoc incorrecte: {self.estoc!r}")

Llibre("Fantasma", -3)     # ValueError: No es permet un preu negatiu: -3
Llibre("  Hamlet ", 9.95)  # OK, i a mes normalitza: titol == "Hamlet"

Les invariants del negoci de Papyrus — preu ≥ 0, estoc enter ≥ 0 — queden protegides en la construcció. (A diferència de les properties de 05-04, __post_init__ no vigila les assignacions posteriors; si necessites això, combina la dataclass amb properties o usa frozen=True, que ve tot seguit.)

frozen=True: dataclasses immutables

@dataclass(frozen=True) genera objectes immutables: qualsevol assignació posterior llança un error, com a les tuples i namedtuples de 04-02.

@dataclass(frozen=True)
class CodiSoci:
    nom: str
    numero: int

lluis = CodiSoci("LLUIS", 1)
lluis.numero = 99          # FrozenInstanceError: cannot assign to field 'numero'

I arriba el premi que 05-05 va deixar pendent: una dataclass congelada sí que és hashejable (genera un __hash__ coherent amb el seu __eq__), així que les seves instàncies entren en sets i com a claus de diccionari (04-04):

socis_actius = {CodiSoci("LLUIS", 1), CodiSoci("MARTA", 2), CodiSoci("LLUIS", 1)}
print(len(socis_actius))    # 2 — el duplicat es fusiona, com a tot set

Criteri: usa frozen=True per a objectes-valor que representen dades identificatives (codis, coordenades, diners); deixa mutables els que modelen estat que canvia (l'estoc d'un llibre).

order=True: comparables i ordenables

@dataclass(order=True) genera __lt__, __le__, __gt__ i __ge__ comparant els camps en el seu ordre de declaració, com les tuples:

@dataclass(order=True)
class Reserva:
    prioritat: int          # primer camp declarat → mana en l'ordre
    client: str = field(compare=False)   # exclos: no desempata per nom

cua = [Reserva(2, "Omar"), Reserva(1, "Julia"), Reserva(2, "Pau")]
for r in sorted(cua):
    print(r.prioritat, r.client)     # 1 Julia · 2 Omar · 2 Pau

El truc professional està a la vista: declara primer el camp pel qual vols ordenar i exclou amb field(compare=False) els que no hi hagin d'influir. És l'alternativa generada a escriure __lt__ a mà (05-05); tria una via o l'altra, no totes dues.

La taula de criteri definitiva: namedtuple vs dict vs classe vs dataclass

A 04-06 vam tancar les estructures de dades amb una taula de criteri. La POO hi afegeix dues columnes i la deixa completa:

dict namedtuple (04-06) Classe normal (05-01…05) @dataclass
Accés d["preu"] llibre.preu llibre.preu llibre.preu
Mutable No Sí (o no, amb frozen)
Mètodes propis No Limitat Sí, sense restriccions Sí, sense restriccions
__repr__/__eq__ dignes Gratis A mà (05-05) Gratis
Validació en crear No No __init__/properties __post_init__
Camps definits i estables No (claus lliures)
Cost en línies 0 1 Alt Baix
Usa'l quan... Dades heterogènies de vida curta, claus dinàmiques Registre immutable trivial, sense comportament Molta lògica, invariants vigilades amb properties, jerarquies riques Dades amb nom + una mica de comportament i validació

Regla de decisió ràpida, de menys a més estructura: claus que canvien en execució? → dict. Només un registre immutable per llegir? → namedtuple (o dataclass congelada si vols validació). Dades amb nom, igualtat assenyada, una mica de comportament? → dataclass, el nou punt de partida per defecte. La lògica domina sobre les dades (properties per tot arreu, jerarquia d'herència amb sobreescriptures, dunder a mida)? → classe completa escrita a mà. I les dues coses es barregen: res no impedeix una dataclass amb properties o una jerarquia amb una dataclass com a base.

El catàleg final de Papyrus: dict d'objectes

Tanquem el cercle obert a 04-03. El gran refactor va deixar el catàleg com a {titol: {"preu": ..., "estoc": ...}}; el mòdul 5 el porta a la seva forma definitiva — diccionari de clau normalitzada a objecte Llibre — combinant el millor de tots dos mons: la cerca directa per clau del dict i la intel·ligència de l'objecte.

from dataclasses import dataclass

NOM_BOTIGA = "Papyrus"

def normalitzar_titol(text):
    """La normalitzacio canonica de 04-05, ara clau del cataleg."""
    return text.strip().casefold()

@dataclass
class Llibre:
    titol: str
    preu: float
    estoc: int = 0

    IVA_LLIBRES = 0.04
    DESCOMPTE_SOCI = 0.05

    def __post_init__(self):
        if self.preu < 0:
            raise ValueError(f"Preu negatiu: {self.preu}")
        if self.estoc < 0:
            raise ValueError(f"Estoc negatiu: {self.estoc}")

    def preu_final(self, soci=False):
        descompte = Llibre.DESCOMPTE_SOCI if soci else 0
        return round(self.preu * (1 - descompte) * (1 + Llibre.IVA_LLIBRES), 2)

    def hi_ha_estoc(self):
        return self.estoc > 0


llibres = [
    Llibre("L'Odissea", 12.50, 4),
    Llibre("Hamlet", 9.95, 6),
    Llibre("El Quixot", 15.90, 8),
    Llibre("Faust", 21.00, 0),
]

# El cataleg definitiu: dict[str, Llibre] — clau normalitzada → objecte
cataleg = {normalitzar_titol(llibre.titol): llibre for llibre in llibres}

def cercar_llibre(cataleg, cercat):
    """La cerca de papyrus_utils, reduida a una linia."""
    return cataleg.get(normalitzar_titol(cercat))

comanda = cercar_llibre(cataleg, "  HAMLET ")
if comanda and comanda.hi_ha_estoc():
    print(f"{comanda.titol}: {comanda.preu_final(soci=True):.2f} € per al Lluis")
    # Hamlet: 9.83 € per al Lluis

Compara-ho amb el punt de partida del mòdul: cercar_llibre() ja no recorre claus normalitzant-les una a una — el catàleg es va construir normalitzat (comprensió de diccionaris, 04-03) i get() (04-03) resol la cerca amb el cas perdut inclòs, retornant None com la versió original. I el que retorna no és un subdiccionari anònim: és un objecte que sap calcular el seu preu i declarar el seu estoc.

flowchart LR
    A["Mòdul 4<br>dict de dicts<br>+ funcions soltes"] --> B["05-01…05-05<br>classe Llibre<br>escrita a mà"] --> C["05-06<br>dataclass Llibre<br>cataleg: dict[str, Llibre]"]

Errors Comuns i Consells

  • Oblidar l'anotació de tipus en un camp: titol = "x" sense : str no és un camp — és un atribut de classe i desapareix de l'__init__. Tot camp de dataclass porta anotació; tota constant de classe, no.
  • Camp sense valor per defecte després d'un que sí que en té: estoc: int = 0 seguit d'autor: str dona TypeError (la mateixa regla que els paràmetres de funció de 03-02). Ordena: primer els obligatoris.
  • Valor per defecte mutable: historial: list = [] llança ValueError en definir la classe. Sempre field(default_factory=list) (o dict, set...).
  • Esperar validació de tipus per les anotacions: Llibre(42, "hola") es crea sense queixes. Les anotacions documenten (i eines del mòdul 8 les comprovaran abans d'executar), però en execució valida __post_init__.
  • frozen=True amb un __post_init__ que assigna: self.titol = self.titol.strip() falla amb FrozenInstanceError fins i tot dins de __post_init__. En les congelades, valida sense reassignar (o usa object.__setattr__, un truc avançat que rarament necessitaràs).
  • Barrejar order=True amb un __lt__ manual: el decorador es nega a generar l'ordre si ja defineixes __lt__ (ValueError). Una sola font de veritat per a l'ordre, no dues.
  • Consell: davant d'una classe nova, comença per @dataclass. Si amb el temps necessita properties, herència complexa o dunder a mida, "ascendeix-la" a classe completa: el codi client ni se n'adonarà, perquè la interfície (llibre.preu, llibre.preu_final()) no canvia.

Exercicis

Exercici 1: Revista com a dataclass

Reescriu la classe Revista de 05-02 com a dataclass amb camps titol, numero, preu i estoc = 0, un __post_init__ que validi que numero és un enter positiu, i el mètode preu_final(soci=False) amb la fórmula canònica. Comprova que Revista("Quimera", 482, 6.50, 10).preu_final(True) retorna 6.42 i que numero=0 llança ValueError.

Exercici 2: línia de venda congelada

Crea la dataclass LiniaVenda amb frozen=True i order=True, camps data (cadena "2026-07-13"), titol i quantia — en aquest ordre, perquè les línies s'ordenin cronològicament. Crea tres línies de dates diferents desordenades, ordena-les amb sorted() i comprova que un set elimina una línia duplicada.

Exercici 3: el catàleg complet, de punta a punta

Usant la dataclass Llibre final i el catàleg dict[str, Llibre], escriu vendre_titol(cataleg, cercat, soci=False) que: cerqui el llibre amb cercar_llibre(); retorni None si no existeix; si existeix però no hi ha estoc, retorni la cadena "ESGOTAT: <títol>"; i si hi ha estoc, descompti 1 unitat i retorni el preu final. Prova amb "hamlet" (soci), "FAUST" i "Rayuela".

Solucions

Solució 1:

@dataclass
class Revista:
    titol: str
    numero: int
    preu: float
    estoc: int = 0

    IVA_LLIBRES = 0.04
    DESCOMPTE_SOCI = 0.05

    def __post_init__(self):
        if not isinstance(self.numero, int) or self.numero <= 0:
            raise ValueError(f"Numero de revista incorrecte: {self.numero!r}")

    def preu_final(self, soci=False):
        descompte = Revista.DESCOMPTE_SOCI if soci else 0
        return round(self.preu * (1 - descompte) * (1 + Revista.IVA_LLIBRES), 2)

print(Revista("Quimera", 482, 6.50, 10).preu_final(True))   # 6.42
Revista("Quimera", 0, 6.50)   # ValueError: Numero de revista incorrecte: 0

Quatre camps, validació i comportament en un terç de l'espai que ocupava la versió manual de 05-02.

Solució 2:

@dataclass(frozen=True, order=True)
class LiniaVenda:
    data: str       # primer camp → governa l'ordre cronologic
    titol: str
    quantia: float

linies = [
    LiniaVenda("2026-07-13", "Hamlet", 9.83),
    LiniaVenda("2026-07-11", "L'Odissea", 12.35),
    LiniaVenda("2026-07-12", "El Quixot", 16.54),
    LiniaVenda("2026-07-11", "L'Odissea", 12.35),   # duplicada
]

for linia in sorted(linies):
    print(linia.data, linia.titol)
# 2026-07-11 L'Odissea · 2026-07-11 L'Odissea · 2026-07-12 El Quixot · 2026-07-13 Hamlet

print(len(set(linies)))   # 3 — frozen ⇒ hashejable ⇒ el set deduplica (04-04)

Les dates yyyy-MM-dd ordenen bé com a text — i al mòdul 6 aquestes línies de venda seran exactament el que escriurem en un CSV.

Solució 3:

def vendre_titol(cataleg, cercat, soci=False):
    llibre = cercar_llibre(cataleg, cercat)
    if llibre is None:
        return None
    if not llibre.hi_ha_estoc():
        return f"ESGOTAT: {llibre.titol}"
    llibre.estoc -= 1
    return llibre.preu_final(soci)

print(vendre_titol(cataleg, "hamlet", soci=True))   # 9.83
print(vendre_titol(cataleg, "FAUST"))               # ESGOTAT: Faust
print(vendre_titol(cataleg, "Rayuela"))             # None
print(cataleg["hamlet"].estoc)                      # 5 — la venda ha descomptat de debo

Observa la col·laboració de tot el mòdul en deu línies: cerca normalitzada (04-05/05-06), objecte que informa del seu estoc (05-01), preu calculat pel mateix llibre (05-01/05-02) i el catàleg com a dict d'objectes. Els tres desenllaços diferents (None, cadena, número) demanen a crits alguna cosa millor que "endevina el tipus retornat" — les excepcions del mòdul 7 donaran la resposta elegant.

Conclusió

El mòdul 5 compleix el que va prometre en tancar el mòdul 4: dades i comportament ja no viuen separats. A 05-01, class, __init__ i self van unir el catàleg amb les seves funcions i cada llibre va aprendre a calcular el seu propi preu de soci; l'herència va aixecar la família ProducteLlibre/RevistaLlibreDigital amb super() repartint allò comú (05-02); el polimorfisme va fer que el terminal de l'Anna cobrés qualsevol article — fins i tot ànecs sense pedigrí com el ValRegal — enviant el mateix missatge (05-03); l'encapsulació va blindar les invariants del negoci amb _interns i properties que rebutgen preus i estocs impossibles (05-04); els mètodes màgics van ensenyar als teus objectes l'idioma natiu de Python — print, ==, sorted, len, in, + — i van saldar el print lleig (05-05); i les dataclasses van eliminar el boilerplate, van validar en néixer amb __post_init__ i van completar la taula de criteri que 04-06 va deixar oberta. El resultat final és el catàleg definitiu de Papyrus: dict[str, Llibre], clau normalitzada a objecte intel·ligent. Però aquest edifici té un defecte que l'Anna descobreix cada nit en apagar l'ordinador: tot — el catàleg, els socis, les línies de venda — viu en memòria i es perd en tancar el programa. Demà, cataleg reneix de zero, amb "Faust" altra vegada sense els exemplars que van arribar. El mòdul 6 resol exactament això: fitxers — text, CSV, JSON — perquè l'estat de Papyrus sobrevisqui entre execucions. Els teus objectes ja saben pensar; ara aprendran a recordar.

Curs de Programació en Python

Mòdul 1: Introducció a Python

Mòdul 2: Estructures de Control

Mòdul 3: Funcions i Mòduls

Mòdul 4: Estructures de Dades

Mòdul 5: Programació Orientada a Objectes

Mòdul 6: Gestió de Fitxers

Mòdul 7: Gestió d'Errors i Excepcions

Mòdul 8: Temes Avançats

Mòdul 9: Proves i Depuració

Mòdul 10: Desenvolupament Web amb Python

Mòdul 11: Ciència de Dades amb Python

Mòdul 12: Projecte Final

© Copyright 2026. Tots els drets reservats