El mòdul 4 va acabar amb un diagnòstic clar: a Papyrus, les dades viuen en un diccionari (cataleg) i el comportament viu en un altre lloc (papyrus_utils.py). preu_final() confia que li passis un número correcte; cercar_llibre() confia que el diccionari tingui la forma esperada. Res no uneix les dues meitats. La programació orientada a objectes (POO) resol exactament això: una classe és un motlle que empaqueta dades i comportament en una sola peça amb nom propi, i cada objecte creat a partir d'aquest motlle porta les seves dades a sobre i sap operar-hi. En aquesta lliçó crearàs la classe Llibre de Papyrus: cada llibre deixarà de ser una entrada de diccionari per convertir-se en un objecte que calcula el seu propi preu de soci.
Contingut
- De diccionaris a classes: per què fer el salt
- Definir una classe amb
classi__init__ self: l'objecte parla de si mateix- Atributs d'instància vs atributs de classe
- Mètodes: comportament que viatja amb les dades
- La classe
Llibrecompleta de Papyrus - Diversos objectes del mateix motlle
- Errors comuns i consells
- Exercicis amb solucions
De diccionaris a classes: per què fer el salt
Recorda com va quedar el catàleg després del gran refactor de 04-03:
I com es calcula un preu, des de papyrus_utils.py:
Funciona, però fixa't en les costures:
- Res no garanteix l'estructura: si algú escriu
{"preu": "12,50"}, cap part del programa no protesta fins que explota en un altre lloc. - El coneixement està dispers: per saber què es pot fer amb un llibre has de remenar dins
papyrus_utils.py. - La funció i la dada no es coneixen:
preu_final()accepta qualsevol número; no sap que treballa amb llibres.
Una classe inverteix la relació: en lloc de funcions que reben dades, tens dades que porten les seves funcions incorporades.
| Enfocament | Dades | Comportament | Qui garanteix la coherència? |
|---|---|---|---|
| Dict + funcions (mòdul 4) | cataleg["Hamlet"] |
papyrus_utils.preu_final(...) |
Ningú: disciplina del programador |
| Classe (aquest mòdul) | hamlet.preu |
hamlet.preu_soci() |
La mateixa classe |
Definir una classe amb class i __init__
La sintaxi mínima:
class Llibre:
"""Un llibre del cataleg de Papyrus."""
def __init__(self, titol, preu, estoc):
self.titol = titol
self.preu = preu
self.estoc = estocDesglossem-ho línia a línia:
class Llibre:— declara la classe. Per convenció PEP 8, els noms de classe van en CapWords (Llibre,LlibreDigital), a diferència de funcions i variables (preu_final,estoc_total).- El docstring funciona igual que a les funcions de 03-01: documenta per a què serveix la classe.
__init__és l'inicialitzador: un mètode especial que Python executa automàticament cada cop que crees un objecte. Rep les dades inicials i les guarda.self.titol = titol— crea un atribut d'instància: una variable que pertany a aquell objecte concret, no a la classe en general.
Per crear un objecte (una instància), crides la classe com si fos una funció:
odissea = Llibre("L'Odissea", 12.50, 4)
hamlet = Llibre("Hamlet", 9.95, 6)
print(odissea.titol) # L'Odissea
print(hamlet.preu) # 9.95
print(odissea.estoc) # 4Observa que no passes self en cridar: escrius Llibre("L'Odissea", 12.50, 4) amb tres arguments, i Python posa l'objecte acabat de crear a self per tu.
self: l'objecte parla de si mateix
self és el paràmetre que rep el mateix objecte sobre el qual s'està treballant. Quan l'Anna escriu odissea.preu_soci(), Python ho tradueix internament a Llibre.preu_soci(odissea): l'objecte viatja com a primer argument.
class Llibre:
def __init__(self, titol, preu, estoc):
self.titol = titol
self.preu = preu
self.estoc = estoc
def hi_ha_estoc(self):
return self.estoc > 0- Dins de
hi_ha_estoc,self.estocsignifica "l'estoc d'aquest llibre". - Si
odissea.estocés 4 ifaust.estocés 0,odissea.hi_ha_estoc()retornaTrueifaust.hi_ha_estoc()retornaFalse— el mateix codi, dades diferents, perquèselfapunta a un objecte diferent a cada crida.
selfno és una paraula reservada: és una convenció universal. El podries anomenar d'una altra manera, però cap programador Python no ho fa. Respecta-la sempre.
Atributs d'instància vs atributs de classe
L'IVA dels llibres i el descompte de soci són iguals per a tots els llibres de Papyrus. No té sentit guardar-los a cada objecte: són atributs de classe, compartits per totes les instàncies.
class Llibre:
IVA_LLIBRES = 0.04 # atribut de classe: compartit
DESCOMPTE_SOCI = 0.05 # atribut de classe: compartit
def __init__(self, titol, preu, estoc):
self.titol = titol # atributs d'instancia: propis de cada objecte
self.preu = preu
self.estoc = estoc| Característica | Atribut d'instància | Atribut de classe |
|---|---|---|
| Es defineix a | __init__, amb self. |
Cos de la classe, sense self |
| Pertany a | Cada objecte per separat | La classe (tots el comparteixen) |
| Exemple a Papyrus | titol, preu, estoc |
IVA_LLIBRES, DESCOMPTE_SOCI |
| Canvia entre objectes | Sí (odissea.estoc ≠ hamlet.estoc) |
No (és el mateix per a tothom) |
| Accés | odissea.preu |
Llibre.IVA_LLIBRES o odissea.IVA_LLIBRES |
print(Llibre.IVA_LLIBRES) # 0.04 — des de la classe
print(odissea.IVA_LLIBRES) # 0.04 — des de la instancia (l'hereta de la classe)Quan accedeixes a odissea.IVA_LLIBRES, Python cerca primer a l'objecte; si no ho troba, puja a la classe. Per això funciona des de tots dos llocs. Fixa't que les constants que al mòdul 1 vivien soltes (NOM_BOTIGA, IVA_LLIBRES) ara troben una llar natural dins la classe a la qual pertanyen conceptualment.
Mètodes: comportament que viatja amb les dades
Un mètode és una funció definida dins d'una classe. La fórmula de preu de Papyrus (base × (1−descompte) × (1+IVA), arrodonida a 2 decimals) deixa de viure a papyrus_utils.py i passa a ser coneixement del mateix llibre:
def preu_soci(self):
"""Preu final per a socis: descompte del 5 % i IVA del 4 %."""
brut = self.preu * (1 - Llibre.DESCOMPTE_SOCI) * (1 + Llibre.IVA_LLIBRES)
return round(brut, 2)Compara les dues crides:
# Modul 4: funcio externa que rep la dada
preu_final(cataleg["L'Odissea"]["preu"], soci=True) # 12.35
# Modul 5: l'objecte s'ho calcula sol
odissea.preu_soci() # 12.35El resultat és idèntic (12.35 €), però el segon estil no es pot equivocar de dada: el mètode sempre opera sobre el preu del seu llibre.
La classe Llibre completa de Papyrus
class Llibre:
"""Un llibre del cataleg de Papyrus."""
IVA_LLIBRES = 0.04
DESCOMPTE_SOCI = 0.05
def __init__(self, titol, preu, estoc):
self.titol = titol
self.preu = preu
self.estoc = estoc
def preu_soci(self):
"""Preu final per a socis, arrodonit a 2 decimals."""
brut = self.preu * (1 - Llibre.DESCOMPTE_SOCI) * (1 + Llibre.IVA_LLIBRES)
return round(brut, 2)
def hi_ha_estoc(self):
"""True si queda almenys un exemplar."""
return self.estoc > 0I en acció, amb els quatre títols canònics:
odissea = Llibre("L'Odissea", 12.50, 4)
hamlet = Llibre("Hamlet", 9.95, 6)
quixot = Llibre("El Quixot", 15.90, 8)
faust = Llibre("Faust", 21.00, 0)
for llibre in (odissea, hamlet, quixot, faust):
disponible = "disponible" if llibre.hi_ha_estoc() else "ESGOTAT"
print(f"{llibre.titol:<12} soci: {llibre.preu_soci():>6.2f} € [{disponible}]")L'Odissea soci: 12.35 € [disponible] Hamlet soci: 9.83 € [disponible] El Quixot soci: 15.71 € [disponible] Faust soci: 20.75 € [ESGOTAT]
Els preus coincideixen exactament amb els que el Lluís (soci LLUIS-001) paga des del mòdul 4: la lògica és la mateixa, només ha canviat de casa.
Diversos objectes del mateix motlle
La distinció clau del dia: la classe és el motlle (s'escriu una vegada); cada instància és un objecte independent amb els seus propis valors.
classDiagram
class Llibre {
+IVA_LLIBRES = 0.04
+DESCOMPTE_SOCI = 0.05
+titol
+preu
+estoc
+preu_soci()
+hi_ha_estoc()
}
Llibre <.. odissea : instància
Llibre <.. hamlet : instància
Llibre <.. faust : instància
class odissea {
titol = "L'Odissea"
preu = 12.50
estoc = 4
}
class hamlet {
titol = "Hamlet"
preu = 9.95
estoc = 6
}
class faust {
titol = "Faust"
preu = 21.00
estoc = 0
}
Cada objecte té identitat pròpia: modificar faust.estoc no afecta odissea.estoc (res de les trampes d'àlies de 04-01, perquè són objectes diferents des de la seva creació). Pots comprovar el tipus de qualsevol objecte amb type():
Un detall que potser ja has notat si has provat print(odissea):
Surt "lleig": Python no sap com mostrar un Llibre en text llegible i ensenya la seva adreça de memòria. Hi ha una solució elegant, però pertany als mètodes màgics de la lliçó 05-05. Fins llavors, imprimeix atributs concrets (llibre.titol) quan necessitis sortida llegible.
Errors Comuns i Consells
- Oblidar
selfa la definició del mètode.def preu_soci():senseselfprovocaTypeError: preu_soci() takes 0 positional arguments but 1 was givenen cridar-lo, perquè Python sempre passa l'objecte com a primer argument. Tot mètode d'instància comença perself. - Oblidar
self.en usar un atribut. Dins d'un mètode,preua seques és una variable local inexistent (NameError); l'atribut de l'objecte ésself.preu. Repassa l'àmbit de variables de 03-01: les regles no han canviat. - Escriure
Llibre()sense arguments quan__init__els exigeix:TypeError: __init__() missing 3 required positional arguments. Els paràmetres de__init__funcionen exactament com els de qualsevol funció (03-02), inclosos els valors per defecte:def __init__(self, titol, preu, estoc=0)permetria crear llibres sense estoc inicial. - Confondre classe amb instància.
Llibre.preu_soci()falla perquè no hi ha cap objecte sobre el qual operar;odissea.preu_soci()funciona. La classe és el plànol; la instància, l'edifici. - Usar un atribut de classe mutable com a valor per instància.
reserves = []al cos de la classe seria compartit per tots els llibres — la mateixa trampa de l'argument mutable per defecte de 03-02. Les llistes i diccionaris per objecte es creen sempre a__init__ambself.reserves = []. - Consell: comença tota classe responent dues preguntes: quines dades descriu? (atributs) i què sap fer amb elles? (mètodes). Si un mètode no fa servir
selfper a res, probablement hauria de ser una funció normal fora de la classe.
Exercicis
Exercici 1: la classe Soci
Papyrus gestiona socis com el Lluís (LLUIS-001) o la Marta (MARTA-002). Crea una classe Soci amb atributs d'instància nom i codi, i un mètode salutacio() que retorni "Hola, Lluis (soci LLUIS-001)". Crea els socis Lluís, Marta (MARTA-002) i Pau (PAU-003) i imprimeix les tres salutacions amb un bucle.
Exercici 2: ampliar Llibre amb vendes
Afegeix a la classe Llibre un mètode vendre(unitats) que resti unitats a l'estoc només si n'hi ha prou, retornant True si la venda s'ha fet i False si no. Prova: la Júlia vol 2 exemplars de "L'Odissea" (estoc 4) i l'Omar vol 1 "Faust" (estoc 0).
Exercici 3: comptador d'instàncies amb atribut de classe
Afegeix a Llibre un atribut de classe total_llibres = 0 que s'incrementi en 1 dins de __init__ cada cop que es crea un llibre (pista: Llibre.total_llibres += 1). Crea els quatre llibres del catàleg i comprova que Llibre.total_llibres val 4.
Solucions
Solució 1:
class Soci:
"""Un soci del club de lectors de Papyrus."""
def __init__(self, nom, codi):
self.nom = nom
self.codi = codi
def salutacio(self):
return f"Hola, {self.nom} (soci {self.codi})"
socis = [Soci("Lluis", "LLUIS-001"), Soci("Marta", "MARTA-002"), Soci("Pau", "PAU-003")]
for soci in socis:
print(soci.salutacio())Cada objecte guarda el seu propi nom i codi; el mètode salutacio() és el mateix per als tres, però self fa que cadascun parli de si mateix.
Solució 2:
class Llibre:
IVA_LLIBRES = 0.04
DESCOMPTE_SOCI = 0.05
def __init__(self, titol, preu, estoc):
self.titol = titol
self.preu = preu
self.estoc = estoc
def hi_ha_estoc(self):
return self.estoc > 0
def vendre(self, unitats):
"""Descompta estoc si n'hi ha prou; retorna True/False."""
if unitats <= self.estoc:
self.estoc -= unitats
return True
return False
odissea = Llibre("L'Odissea", 12.50, 4)
faust = Llibre("Faust", 21.00, 0)
print(odissea.vendre(2)) # True → la Julia s'endu 2 exemplars
print(odissea.estoc) # 2
print(faust.vendre(1)) # False → l'Omar es queda sense Faust (a la cua de reserves de 04-06)
print(faust.estoc) # 0Fixa't que la comprovació protegeix la dada: l'estoc mai no pot quedar negatiu si tothom fa servir vendre(). Que ningú no se'l pugui saltar és justament el tema de l'encapsulació (05-04).
Solució 3:
class Llibre:
total_llibres = 0 # atribut de classe: comptador compartit
def __init__(self, titol, preu, estoc):
self.titol = titol
self.preu = preu
self.estoc = estoc
Llibre.total_llibres += 1 # es toca via la CLASSE, no via self
odissea = Llibre("L'Odissea", 12.50, 4)
hamlet = Llibre("Hamlet", 9.95, 6)
quixot = Llibre("El Quixot", 15.90, 8)
faust = Llibre("Faust", 21.00, 0)
print(Llibre.total_llibres) # 4El comptador viu a la classe, per això totes les instàncies el comparteixen i cada creació l'incrementa. Si escrivissis self.total_llibres += 1, crearies un atribut d'instància que taparia el de classe — un error clàssic.
Conclusió
Avui has unit el que el mòdul 4 va deixar separat: la classe Llibre empaqueta les dades (titol, preu, estoc) amb el seu comportament (preu_soci(), hi_ha_estoc()) en una sola peça. Has après que class defineix el motlle, __init__ inicialitza cada objecte, self permet que cada instància parli de si mateixa, i que els atributs d'instància (diferents per objecte) conviuen amb els de classe (IVA_LLIBRES, DESCOMPTE_SOCI, compartits per tothom). Cada llibre de Papyrus calcula ara el seu propi preu de soci: 12.35 € per a "L'Odissea", 20.75 € per a l'esgotat "Faust". Però la botiga de l'Anna no ven només llibres: també hi ha revistes i, aviat, llibres digitals sense estoc físic. Caldrà copiar i enganxar la classe Llibre tres vegades? De cap manera: la lliçó següent presenta l'herència, el mecanisme amb què una classe Producte compartirà allò comú i cada tipus d'article afegirà només el que el fa diferent.
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
