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

  1. De diccionaris a classes: per què fer el salt
  2. Definir una classe amb class i __init__
  3. self: l'objecte parla de si mateix
  4. Atributs d'instància vs atributs de classe
  5. Mètodes: comportament que viatja amb les dades
  6. La classe Llibre completa de Papyrus
  7. Diversos objectes del mateix motlle
  8. Errors comuns i consells
  9. 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:

cataleg = {
    "L'Odissea": {"preu": 12.50, "estoc": 4},
    "Hamlet": {"preu": 9.95, "estoc": 6},
}

I com es calcula un preu, des de papyrus_utils.py:

preu = preu_final(cataleg["L'Odissea"]["preu"], soci=True)

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 = estoc

Desglossem-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)   # 4

Observa 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.estoc significa "l'estoc d'aquest llibre".
  • Si odissea.estoc és 4 i faust.estoc és 0, odissea.hi_ha_estoc() retorna True i faust.hi_ha_estoc() retorna False — el mateix codi, dades diferents, perquè self apunta a un objecte diferent a cada crida.

self no é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.estochamlet.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.35

El 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 > 0

I 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():

print(type(odissea))             # <class '__main__.Llibre'>
print(type(odissea) is Llibre)   # True

Un detall que potser ja has notat si has provat print(odissea):

print(odissea)   # <__main__.Llibre object at 0x0000021F3A2B4E50>

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 self a la definició del mètode. def preu_soci(): sense self provoca TypeError: preu_soci() takes 0 positional arguments but 1 was given en cridar-lo, perquè Python sempre passa l'objecte com a primer argument. Tot mètode d'instància comença per self.
  • Oblidar self. en usar un atribut. Dins d'un mètode, preu a seques és una variable local inexistent (NameError); l'atribut de l'objecte és self.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__ amb self.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 self per 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)         # 0

Fixa'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)   # 4

El 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

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