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
- El boilerplate que tothom escriu igual
@dataclass: tres mètodes gratis- Valors per defecte i
field(default_factory=...) __post_init__: validar en néixerfrozen=True: dataclasses immutablesorder=True: comparables i ordenables- La taula de criteri definitiva: namedtuple vs dict vs classe vs dataclass
- El catàleg final de Papyrus:
dictd'objectes - Errors comuns i consells
- 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 = 0Això é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: stropreu: floatsón type hints (pistes de tipus), que estudiarem a fons al mòdul 8. De moment en tens prou amb això:@dataclassles 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 canonicaDetall 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 historialLa 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 setCriteri: 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 PauEl 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 | Sí | No | Sí | 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) | Sí | Sí | Sí |
| 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 LluisCompara-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: strno é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 = 0seguit d'autor: strdonaTypeError(la mateixa regla que els paràmetres de funció de 03-02). Ordena: primer els obligatoris. - Valor per defecte mutable:
historial: list = []llançaValueErroren definir la classe. Semprefield(default_factory=list)(odict,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=Trueamb un__post_init__que assigna:self.titol = self.titol.strip()falla ambFrozenInstanceErrorfins i tot dins de__post_init__. En les congelades, valida sense reassignar (o usaobject.__setattr__, un truc avançat que rarament necessitaràs).- Barrejar
order=Trueamb 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: 0Quatre 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 deboObserva 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 Producte → Llibre/Revista → LlibreDigital 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
- Introducció a Python
- Configuració de l'Entorn de Desenvolupament
- Sintaxi de Python i Tipus de Dades Bàsics
- Variables i Constants
- Entrada i Sortida Bàsica
- Entorns Virtuals i Gestió de Paquets
Mòdul 2: Estructures de Control
Mòdul 3: Funcions i Mòduls
- Definició de Funcions
- Arguments de Funció
- Funcions Lambda
- Mòduls i Paquets
- Visió General de la Biblioteca Estàndard
Mòdul 4: Estructures de Dades
Mòdul 5: Programació Orientada a Objectes
Mòdul 6: Gestió de Fitxers
- Lectura i Escriptura de Fitxers
- Treball amb Fitxers CSV
- Gestió de Dades JSON
- Operacions amb Fitxers i Directoris
Mòdul 7: Gestió d'Errors i Excepcions
- Introducció a les Excepcions
- Gestió d'Excepcions
- Llançament d'Excepcions
- Excepcions Personalitzades
- Bones Pràctiques i Registre d'Errors amb logging
Mòdul 8: Temes Avançats
- Anotacions de Tipus (type hints)
- Decoradors
- Generadors
- Gestors de Context
- Concurrència: Fils i Processos
- Asyncio per a la Programació Asíncrona
Mòdul 9: Proves i Depuració
- Introducció a les Proves
- Proves Unitàries amb unittest
- Proves amb pytest
- Desenvolupament Guiat per Proves
- Tècniques de Depuració
- Ús de pdb per a la Depuració
Mòdul 10: Desenvolupament Web amb Python
- Introducció al Desenvolupament Web
- Fonaments del Framework Flask
- Construcció d'APIs REST amb Flask
- Introducció a Django
- Construcció d'Aplicacions Web amb Django
Mòdul 11: Ciència de Dades amb Python
- Introducció a la Ciència de Dades
- NumPy per a la Computació Numèrica
- Pandas per a la Manipulació de Dades
- Matplotlib per a la Visualització de Dades
- Introducció a l'Aprenentatge Automàtic amb scikit-learn
