Tancàvem el mòdul 6 amb una incomoditat: TascaFàcil ja sap cercar, ordenar, recórrer arbres i mesurar el seu propi cost, però tot això es recolza en una llista de diccionaris que ningú no vigila. Res no impedeix que a un diccionari li falti la clau completada, que un altre tingui prioritat escrita com a "Alta" o que un tercer arrossegui un camp inventat; i les funcions que treballen amb tasques —mostrar_fitxa, dies_totals, clau_ordre— estan escampades pel fitxer, lluny de les dades que manipulen. En aquesta lliçó comencem a arreglar les dues coses alhora.

L'eina es diu programació orientada a objectes, i ja la vam anomenar de passada a Llenguatges de programació com un dels grans paradigmes. Aquí la faràs servir per primera vegada amb un objectiu molt concret: deixar de tenir dades per una banda i funcions per l'altra per passar a tenir coses —tasques, agendes— que porten dins tant el que són com el que saben fer. Començarem pel més essencial: què és una classe, què és una instància i en què es diferencien d'un diccionari.

Contingut

  1. L'agenda fràgil: una demostració incòmoda
  2. Què és un objecte: dades i comportament junts
  3. Classe i instància: el motlle i els exemplars
  4. La sintaxi mínima: class i creació d'instàncies
  5. Atributs assignats des de fora (i per què encara no n'hi ha prou)
  6. type(), isinstance() i «en Python tot és un objecte»
  7. Atributs de classe enfront d'atributs d'instància
  8. L'espai de noms de l'objecte: __dict__, getattr, setattr i hasattr
  9. Diccionari o objecte: quan compensa cadascun
  10. Els quatre pilars de la POO, en una taula
  11. Errors comuns i consells
  12. Exercicis
  13. Conclusió

  1. L'agenda fràgil: una demostració incòmoda

Abans de presentar la solució, veiem el problema executant-se. Aquesta és una agenda com les que fem servir des de 05-04, però amb tres tasques que han arribat de llocs diferents: una la va escriure un programador a mà, una altra va venir d'un JSON antic i la tercera la va crear un company que no coneixia el format.

agenda = [
    {"titol": "Cartell fira del llibre", "responsable": "Luis",
     "prioritat": "alta", "dies": 3, "completada": False},
    {"titol": "Logotip Forn Sole", "responsable": "Nuria",
     "prioritat": "Alta", "dies": 5, "completada": False},   # <-- 'Alta' amb majuscula
    {"titol": "Pressupost client Vidal", "responsable": "Marta",
     "prioritat": "mitjana", "dies": 2, "urgent": True},     # <-- falta 'completada'
]                                                            #     i sobra 'urgent'

def mostrar_llistat(agenda):
    for numero, tasca in enumerate(agenda, start=1):
        marca = "[X]" if tasca["completada"] else "[ ]"
        print(f"{numero:>2}. {marca} {tasca['titol']:<26}{tasca['prioritat']}")

mostrar_llistat(agenda)

#  1. [ ] Cartell fira del llibre   alta
#  2. [ ] Logotip Forn Sole         Alta
# Traceback (most recent call last):  ...  KeyError: 'completada'

Fixa't en la forma exacta del desastre, perquè és el que justifica tot el mòdul:

  • El programa va imprimir mig llistat abans de petar, i l'error apareix lluny de la seva causa: el KeyError salta a mostrar_llistat, però la falla es va cometre molt abans, quan algú va crear aquell diccionari sense completada. En un programa gran els poden separar centenars de línies i uns quants dies.
  • El segon problema ni tan sols va donar error. "Alta" es va imprimir tan tranquil·la. El desgavell arribarà més tard, quan clau_ordre faci ORDRE_PRIORITAT["Alta"], o quan un filtre tasca["prioritat"] == "alta" deixi aquella tasca fora sense avisar de res. Aquest és el pitjor cas: un resultat incorrecte que sembla correcte.
  • El camp urgent és brossa silenciosa. Ningú no el llegeix ni l'actualitza, però hi és, es desa al JSON i confon qui obri el fitxer d'aquí a sis mesos.

L'arrel comuna és que enlloc del programa no hi ha escrit què és una tasca. El format només existeix al nostre cap i en el costum. Un diccionari accepta qualsevol clau i qualsevol valor: aquella flexibilitat, que a 05-03 era una virtut, aquí és exactament el problema.

  1. Què és un objecte: dades i comportament junts

Un objecte és un element del programa que reuneix dues coses que fins ara portàvem separades:

Part Nom tècnic A TascaFàcil
El que l'objecte és (les seves dades) Atributs títol, responsable, prioritat, dies, completada
El que l'objecte sap fer Mètodes mostrar-se, marcar-se com a completada, dir si és urgent

Fins ara el nostre programa tenia les dades en diccionaris i el comportament en funcions escampades per tascafacil.py. Res no connectava mostrar_fitxa amb el diccionari que sap pintar, tret de l'esperança que qui la cridi li passi el tipus de dada adequat.

El més curiós és que ja has fet servir objectes durant tot el curs sense anomenar-los així. Quan escrius titol.upper() o titol.count("e"), titol no és «només» un text: és un objecte que conté uns caràcters i a més sap posar-se en majúscules, comptar lletres o partir-se en trossos. Aquell .upper() és un mètode: una funció que viu dins de l'objecte i actua sobre les seves pròpies dades. Per això no cal escriure upper(titol): l'objecte ja sap sobre quin text ha de treballar. El mateix amb les llistes (agenda.append(...)) i els diccionaris (tasca.get("dies")). La notació del punt que fas servir des del mòdul 2 és, literalment, la notació de l'orientació a objectes: objecte.elQueSapFer().

El que aprendràs en aquest mòdul és a crear els teus propis tipus d'objecte, perquè una tasca d'Estudi Alba tingui tant sentit per a Python com en té un text o una llista.

  1. Classe i instància: el motlle i els exemplars

Aquí apareixen les dues paraules centrals del mòdul, i convé no confondre-les mai. Una classe és la definició: descriu quines dades té una tasca i què sap fer, s'escriu una sola vegada i no és una tasca concreta, sinó la idea de tasca. Una instància (o objecte) és cada exemplar creat a partir d'aquesta classe: se'n creen tantes com calgui i cadascuna té els seus propis valors.

L'analogia que millor funciona és la del formulari en paper. La classe és la plantilla impresa: defineix les caselles «títol», «responsable», «prioritat», «dies» i «completada». La instància és cada formulari emplenat: un diu «Cartell fira del llibre / Luis / alta / 3 / no», un altre diu «Logotip Forn Solé / Nuria / alta / 5 / no». Hi ha una plantilla i molts formularis, i canviar la plantilla canvia tots els formularis futurs.

flowchart TD
    T["CLASSE Tasca (el motlle)<br/>titol, responsable, prioritat, dies, completada"]
    T -->|instancia de| C["cartell<br/>Cartell fira del llibre / Luis / alta / 3"]
    T -->|instancia de| L["logotip<br/>Logotip Sole / Nuria / alta / 5"]
    T -->|instancia de| P["pressupost<br/>Pressupost Vidal / Marta / mitjana / 2"]

Dues conseqüències pràctiques convé fixar-les des del principi: la classe s'escriu una vegada i serveix per a un milió de tasques (si demà tota tasca necessita un camp client, es toca la classe, no les mil crides escampades pel programa), i les instàncies són independents entre si (marcar una tasca com a completada no toca les altres, igual que emplenar un formulari no emplena els de la resta de la carpeta).

  1. La sintaxi mínima: class i creació d'instàncies

La forma més simple d'una classe en Python, i la creació de dues instàncies a partir d'ella, són aquestes:

class Tasca:
    """Una tasca de l'estudi de disseny Estudi Alba."""
    pass

cartell = Tasca()
logotip = Tasca()
print(cartell)             # <__main__.Tasca object at 0x7f8b1c0d5e50>
print(cartell is logotip)  # False: son dos objectes diferents

Tres detalls de sintaxi, que són els que fallen al principi:

  • La paraula reservada és class, seguida del nom i de dos punts, i el cos va indentat, igual que en un def o en un if.
  • El nom de les classes s'escriu per conveni en CamelCase: Tasca, TascaRecurrent, Agenda. Les funcions i les variables continuen en snake_case (registrar_tasca). Veure-ho així et diu d'un cop d'ull si un nom és una classe.
  • pass és el farciment per a un cos buit; el docstring de la primera línia compleix aquí la mateixa funció que a les funcions.

Escriure Tasca() —amb parèntesis, com una crida a funció— fabrica un objecte nou d'aquesta classe i retorna una referència a ell. A aquesta operació se l'anomena instanciar. Cada crida crea un objecte diferent a la memòria, i per això cartell is logotip és False, encara que ara mateix tots dos estiguin igual de buits: és el mateix comportament de les llistes que vas veure a 05-01, on [] is [] també era False.

Aquell <__main__.Tasca object at 0x...> que imprimeix print és la representació per defecte: diu de quina classe és i en quina adreça de memòria viu. És lletja expressament; a Atributs, mètodes i constructor aprendràs a substituir-la per alguna cosa llegible amb __str__.

  1. Atributs assignats des de fora (i per què encara no n'hi ha prou)

A una instància se li poden afegir atributs amb la notació del punt, sense declarar-los prèviament:

cartell = Tasca()
cartell.titol = "Cartell fira del llibre"
cartell.responsable = "Luis"
cartell.prioritat = "alta"
cartell.dies = 3
cartell.completada = False
print(f"{cartell.titol} ({cartell.dies}d)")   # Cartell fira del llibre (3d)

Compara'n la lectura amb la del diccionari equivalent:

Operació Diccionari Objecte
Llegir un camp tasca["titol"] tasca.titol
Escriure un camp tasca["dies"] = 4 tasca.dies = 4
Camp inexistent KeyError AttributeError
Camps possibles qualsevol, sense control els que es defineixin (veure 07-02)

Ja s'hi guanya alguna cosa: cartell.titol es llegeix millor que cartell["titol"], i l'editor pot suggerir-te els atributs disponibles mentre escrius. Però siguem honestos: això encara no resol el problema de la secció 1. Res no ens obliga a emplenar els cinc camps:

logotip = Tasca()
logotip.titol = "Logotip Forn Sole"
logotip.prioritat = "Alta"        # un altre cop amb majuscula, ningu no protesta
print(logotip.completada)         # ... i ens haviem descuidat aquest camp:
# AttributeError: 'Tasca' object has no attribute 'completada'

Hem canviat un KeyError per un AttributeError, i poca cosa més: la falla continua apareixent tard i continua sense haver-hi un lloc que garanteixi que tota tasca neix completa i amb valors vàlids. Aquest lloc existeix i es diu constructor: un bloc que Python executa automàticament en crear cada instància, que exigeix les dades imprescindibles i pot validar-les abans de desar-les. És el __init__ que estudiarem a 07-02, i és el que converteix la classe d'un simple contenidor en una garantia. Queda't amb la idea: definir la classe és el primer pas; fer que sigui impossible crear una tasca invàlida és el segon.

  1. type(), isinstance() i «en Python tot és un objecte»

type(), que vam fer servir a 02-01 per descobrir tipus de dades, també funciona amb les nostres classes, i per preguntar si un objecte és d'una certa classe es fa servir isinstance():

cartell = Tasca()
print(type(cartell))                  # <class '__main__.Tasca'>
print(type(cartell).__name__)         # Tasca
print(isinstance(cartell, Tasca))     # True
print(isinstance(cartell, dict))      # False
print(isinstance(3.5, (int, float)))  # True: admet una tupla de tipus

La manera correcta de comprovar un tipus és isinstance(x, Classe), no type(x) == Classe: a més de llegir-se millor, reconeix les classes derivades, cosa que veurem al final de Col·leccions d'objectes.

Ara l'afirmació que dona sentit a tot: en Python, absolutament tot és un objecte. No és una frase de manual, es comprova en quatre línies:

print(type(5))        # <class 'int'>    -> 5 es instancia de la classe int
print(type("hola"))   # <class 'str'>    -> "hola" es instancia de str
print(type([1, 2]))   # <class 'list'>   -> la llista, de list
print(type(Tasca))    # <class 'type'>   -> fins i tot les classes son objectes
Expressió Què demostra
"hola".upper() str és una classe i upper és un dels seus mètodes
(255).bit_length() fins i tot un nombre enter té mètodes propis
[3, 1].sort() sort és un mètode de la classe list

És a dir: no estàs aprenent una tècnica exòtica afegida al llenguatge, estàs aprenent com està construït Python per dins, i l'única cosa nova és que ara els motlles els defineixes tu. Tot el que saps de str o de list —que es passen per referència, que tenen mètodes, que type() els identifica— val igual per a Tasca.

  1. Atributs de classe enfront d'atributs d'instància

Hi ha dos llocs on pot viure un atribut, i la diferència importa. Un atribut d'instància pertany a un objecte concret i cada instància té el seu: és el cartell.titol de la secció 5. Un atribut de classe es defineix dins del cos de la classe i és compartit per totes les instàncies: n'hi ha una sola còpia, a la classe. El cas d'ús clàssic és un valor comú a totes les tasques i un comptador de quantes se n'han creat:

class Tasca:
    """Una tasca de l'estudi Estudi Alba."""
    ESTUDI = "Estudi Alba"      # atribut de classe: igual per a totes
    creades = 0                 # atribut de classe: comptador compartit

cartell, logotip = Tasca(), Tasca()
Tasca.creades += 2             # normalment es fara al constructor (07-02)

print(Tasca.creades)           # 2
print(cartell.creades)         # 2  <- la instancia veu l'atribut de la classe
print(logotip.ESTUDI)          # Estudi Alba

cartell.creades = 99           # ATENCIO: crea un atribut PROPI de cartell
print(cartell.creades)         # 99  <- el seu
print(logotip.creades)         # 2   <- continua veient el de la classe
print(Tasca.creades)           # 2   <- la classe no s'ha tocat

Una instància que no té un atribut propi el busca a la seva classe: per això cartell.creades funciona encara que mai no l'hi hàgim assignat a cartell. És un ordre de cerca semblant al LEGB del mòdul 4: primer el que és propi, després el que s'hereta del motlle. I aquí hi ha el parany: assignar a través de la instància no modifica la classe, sinó que crea un atribut d'instància que la tapa. Per això el comptador s'incrementa sempre anomenant la classe, Tasca.creades += 1.

L'avís important: no posis mai un objecte mutable com a atribut de classe si esperes que cada instància tingui el seu. És l'aliasing de 05-01 en la seva versió més traïdora:

class Tasca:
    notes = []            # MALAMENT: una unica llista per a totes les tasques

cartell, logotip = Tasca(), Tasca()
cartell.notes.append("Falta el logo de la fira")
print(logotip.notes)      # ['Falta el logo de la fira'] <- la nota d'una altra tasca

Com que cartell.notes no troba llista pròpia, puja a la classe i modifica la llista compartida, que és la mateixa que veu logotip. La regla pràctica és senzilla:

Tipus de dada Val com a atribut de classe?
Constants immutables (str, int, tuple) Sí: ESTUDI, PRIORITATS = ("alta", "mitjana", "baixa")
Comptadors que vols compartir expressament Sí, actualitzant-los amb Classe.nom
Llistes, diccionaris i conjunts per instància No: es creen al constructor (07-02)

  1. L'espai de noms de l'objecte: __dict__, getattr, setattr i hasattr

Per dins, els atributs d'instància es desen en... un diccionari. Cada objecte té el seu, accessible com a __dict__:

cartell = Tasca()
cartell.titol = "Cartell fira del llibre"
cartell.dies = 3
print(cartell.__dict__)     # {'titol': 'Cartell fira del llibre', 'dies': 3}
print(vars(cartell))        # el mateix: vars() es la forma elegant de demanar-ho

Això explica de cop unes quantes coses: per què es poden afegir atributs sobre la marxa, per què accedir a un atribut costa O(1) com qualsevol clau de diccionari (mòdul 6), i per què convertir un objecte a diccionari per desar-lo en JSON serà tan directe a 07-03. Fixa't que __dict__ només conté els atributs d'instància: ESTUDI i creades no hi apareixen perquè viuen a la classe.

Quan el nom de l'atribut està en una variable, hi ha tres funcions per treballar-hi:

print(hasattr(cartell, "dies"))                # True:  l'objecte te aquest atribut
print(hasattr(cartell, "completada"))          # False: no el te
print(getattr(cartell, "completada", False))   # False: valor per defecte, no falla
setattr(cartell, "completada", True)           # equival a cartell.completada = True
Funció Equival a Per a què serveix de veritat
getattr(obj, "x") obj.x Llegir un atribut el nom del qual està en una variable
getattr(obj, "x", defecte) Llegir sense arriscar-se a un AttributeError
setattr(obj, "x", v) obj.x = v Escriure un atribut amb nom calculat
hasattr(obj, "x") Comprovar abans de llegir

En el dia a dia escriuràs cartell.dies, no getattr(cartell, "dies"): aquestes funcions són per al cas en què el nom no es coneix fins que el programa s'executa. Un exemple amb TascaFàcil, aprofitant la constant CAMPS que ja existeix des de 05-05:

CAMPS = ("titol", "responsable", "prioritat", "dies", "completada")

def revisar(tasca):
    """Indica quins camps obligatoris li falten a una tasca."""
    return [camp for camp in CAMPS if not hasattr(tasca, camp)]

print(revisar(cartell))    # ['responsable', 'prioritat']

És un pedaç útil, però continua sent una comprovació posterior: detecta el problema quan la tasca invàlida ja existeix. La solució de veritat és impedir que neixi així, i per a això cal el constructor.

  1. Diccionari o objecte: quan compensa cadascun

Crear una classe no sempre és la resposta correcta. Els diccionaris continuen sent l'eina adequada en molts casos, i aquesta és la comparació honesta:

Criteri Diccionari Objecte (classe pròpia)
Definició del format No existeix: cadascú hi posa el que vol Escrita una vegada a la classe
Accés t["titol"] (falla en execució) t.titol (l'editor autocompleta i avisa)
Garantia de camps Cap El constructor els exigeix (07-02)
Validació de valors Manual, a cada lloc Centralitzada en crear (07-02)
Comportament associat Funcions escampades pel fitxer Mètodes al costat de les dades
Claus dinàmiques Sí, és el seu punt fort No: els atributs són fixos
Desar en JSON Directe Cal convertir-lo abans (07-03)
Cost d'escriure-ho Zero línies Unes quantes línies de classe

D'aquí en surten dues regles pràctiques. Fes servir un diccionari quan les dades vinguin de fora amb forma variable (un JSON descarregat, una fila de CSV), quan les claus no es coneguin per endavant (un índex responsable → tasques, un comptador de paraules, la taula de decisió de 05-04) o quan sigui una estructura temporal que viu tres línies dins d'una funció.

Fes servir una classe quan el concepte es repeteixi per tot el programa amb la mateixa forma (una tasca, un client, una factura), quan hi hagi invariants a complir (la prioritat només pot ser alta, mitjana o baixa; els dies, entre 1 i 365), quan hi hagi comportament que pertanyi a aquestes dades (saber si està endarrerida, marcar-se com a completada, imprimir-se) o quan el concepte hagi de créixer: avui són cinc camps, demà en seran vuit i tres regles.

La tasca de TascaFàcil compleix els quatre criteris de la segona llista, així que s'emporta la classe. L'agenda que arriba del JSON continuarà sent, en el moment de la lectura, una llista de diccionaris: la convertirem en objectes just després.

  1. Els quatre pilars de la POO, en una taula

L'orientació a objectes es resumeix tradicionalment en quatre idees. Aquí van anomenades, perquè reconeguis els termes quan els llegeixis fora del curs:

Pilar En una línia On apareix en aquest curs
Abstracció Representar un concepte real amb l'essencial i res més Aquesta lliçó i 07-02
Encapsulació Desar dades i comportament junts i controlar-ne l'accés 07-02 i 07-03
Herència Crear una classe a partir d'una altra, reaprofitant el que fa 07-03, només el bàsic
Polimorfisme Tractar igual objectes diferents que comparteixen interfície 07-03, només el bàsic

Un avís d'expectatives: aquest curs és de fonaments i arriba fins a la base. Veuràs abstracció i encapsulació amb detall perquè són les que resolen el problema que tenim; herència i polimorfisme es presentaran en la seva forma més simple i amb la recomanació de no abusar-ne. Tota la resta —classes abstractes, herència múltiple, patrons de disseny— és material d'un curs específic de POO, i no el necessites per escriure programes correctes i ordenats.

Errors Comuns i Consells

  • Confondre la classe amb la instància. Tasca és el motlle; Tasca() fabrica un exemplar. Si escrius cartell = Tasca (sense parèntesis), cartell no és una tasca: és la classe mateixa, i cartell.titol = "..." modificaria el motlle per a tot el programa. Comprova-ho amb type(cartell): ha de dir <class '__main__.Tasca'>, no <class 'type'>.
  • Esperar que la classe validi alguna cosa tota sola. Una classe buida no protegeix de res; només dona un nom. La garantia arriba amb el constructor de 07-02.
  • Posar una llista o un diccionari com a atribut de classe, o assignar a instancia.comptador creient que actualitzes la classe. Són les dues cares de l'error de la secció 7: allò mutable compartit es contagia entre instàncies i allò assignat per instància no arriba mai a la classe. Els mutables, sempre per instància; el comptador compartit, sempre com a Tasca.creades += 1.
  • Escriure classes per a tot. Una classe amb dos camps, sense comportament i feta servir en un sol lloc és pitjor que un diccionari: més línies per al mateix resultat. Aplica la taula de la secció 9.
  • Noms poc clars. La classe s'anomena en singular i descriu una cosa (Tasca, no Tasques ni GestorDeTasques), en CamelCase. El plural es reserva per a les col·leccions.
  • Consell: prova les classes a la consola interactiva. Crea una instància, assigna-li atributs, mira'n el __dict__, pregunta isinstance. Veure l'objecte per dins és el que converteix aquests conceptes en alguna cosa concreta.

Exercicis

Exercici 1: Detectar tasques mal formades

Partint de l'agenda de diccionaris de la secció 1, escriu una funció validar_agenda(agenda) que recorri la llista i retorni una llista de textos descrivint tots els problemes trobats: camps que falten, camps que sobren i prioritats que no estiguin exactament a ("alta", "mitjana", "baixa"). Ha de revisar l'agenda sencera, sense aturar-se a la primera falla.

Exercici 2: Del diccionari a l'objecte

Defineix una classe Client amb un atribut de classe ESTUDI = "Estudi Alba" i un comptador creats. Crea dues instàncies (Forn Solé i client Vidal), assigna'ls des de fora els atributs nom, contacte i actiu, incrementa el comptador a cada creació i mostra el __dict__ de cadascuna juntament amb el total de clients creats. Després, assigna forn.creats = 50 i explica per escrit què imprimeix Client.creats i per què.

Exercici 3: Decidir l'estructura

Per a cada cas, decideix si faries servir un diccionari o una classe, i justifica-ho en una frase recolzant-te en la taula de la secció 9: (a) el resultat de comptar quantes vegades apareix cada paraula en els títols de l'agenda; (b) una factura de l'estudi, amb número, client, línies i import, que s'emet, s'envia i es cobra; (c) les dades de configuració llegides d'un fitxer JSON en arrencar; (d) un membre de l'equip, amb nom, hores setmanals disponibles i la capacitat de dir si està sobrecarregat.

Solucions

Solució 1.

CAMPS = ("titol", "responsable", "prioritat", "dies", "completada")
PRIORITATS = ("alta", "mitjana", "baixa")

def validar_agenda(agenda):
    """Retorna la llista de problemes trobats en una agenda de diccionaris."""
    problemes = []
    for numero, tasca in enumerate(agenda, start=1):
        for camp in CAMPS:                            # camps que falten
            if camp not in tasca:
                problemes.append(f"Tasca {numero}: falta el camp '{camp}'")
        for clau in tasca:                            # camps que sobren
            if clau not in CAMPS:
                problemes.append(f"Tasca {numero}: camp desconegut '{clau}'")
        prioritat = tasca.get("prioritat")            # get: no falla si no hi es
        if prioritat is not None and prioritat not in PRIORITATS:
            problemes.append(f"Tasca {numero}: prioritat invalida '{prioritat}'")
    return problemes

for avis in validar_agenda(agenda):
    print(avis)

# Tasca 2: prioritat invalida 'Alta'
# Tasca 3: falta el camp 'completada'
# Tasca 3: camp desconegut 'urgent'

Tres detalls: es fa servir .get("prioritat") en lloc de tasca["prioritat"] perquè la clau podria faltar i no volem un KeyError dins del mateix validador; s'acumula en una llista en comptes d'imprimir, perquè la funció continuï sent pura i separada de la sortida (mòdul 4); i enumerate(..., start=1) numera les tasques tal com les veu l'usuari. Ara fixa't en el que té d'incòmode el resultat: d'aquesta funció cal recordar-se'n de cridar-la, i si algú crea una tasca després, ningú no la revisa. Amb el constructor de 07-02 aquesta comprovació passa a ser automàtica i ineludible.

Solució 2.

class Client:
    """Un client de l'estudi."""
    ESTUDI = "Estudi Alba"
    creats = 0

def alta(nom, contacte, actiu):
    """Crea un client assignant els seus atributs des de fora i compta l'alta."""
    client = Client()
    client.nom, client.contacte, client.actiu = nom, contacte, actiu
    Client.creats += 1
    return client

forn = alta("Forn Sole", "[email protected]", True)
vidal = alta("Client Vidal", "[email protected]", False)

print(forn.__dict__)
print(vidal.__dict__)
print(f"{Client.ESTUDI} compta amb {Client.creats} clients registrats.")

forn.creats = 50
print(forn.creats, Client.creats)            # 50 2

Client.creats continua valent 2. L'assignació forn.creats = 50 no toca la classe: crea un atribut d'instància dins del __dict__ de forn que a partir d'ara tapa el de la classe per a aquell objecte concret. vidal.creats continua llegint el de la classe i val 2. Comprovació directa: print(forn.__dict__) ara inclou 'creats': 50, mentre que el de vidal no.

Solució 3.

Cas Elecció Motiu
(a) Recompte de paraules Diccionari Claus dinàmiques i desconegudes per endavant; estructura temporal sense comportament
(b) Factura Classe Concepte repetit, amb invariants (l'import ha de quadrar) i comportament propi (emetre, enviar, cobrar)
(c) Configuració del JSON Diccionari Arriba de fora amb forma variable i només es llegeix; convertir-la a classe afegeix feina sense guanyar res
(d) Membre de l'equip Classe Forma fixa, es repeteix per tot el programa i té comportament propi: esta_sobrecarregat()

El criteri que més pes té és el de l'última columna: si el concepte fa coses, vol ser una classe; si només transporta dades de forma variable, un diccionari ja li basta.

Conclusió

Un objecte reuneix en un mateix lloc les dades (atributs) i el comportament (mètodes) que van junts, i fa temps que en fas servir des del mòdul 2 sense saber-ho: "hola".upper() i agenda.append(...) són exactament això. La classe és el motlle que defineix com són els objectes d'un tipus, s'escriu una vegada amb class Tasca: en CamelCase, i cada instància es fabrica cridant-la com una funció, Tasca(). Els atributs es llegeixen i s'escriuen amb la notació del punt i viuen realment al __dict__ de l'objecte, amb getattr, setattr i hasattr per quan el nom no es coneix fins a l'execució. Els atributs de classe són compartits per totes les instàncies —perfectes per a constants i comptadors, perillosos si són mutables—, mentre que els d'instància pertanyen a un sol objecte i tapen els de la classe quan coincideixen. type() i isinstance() identifiquen de quina classe és cada cosa i demostren que en Python tot és un objecte, incloses les mateixes classes. I l'elecció entre diccionari i classe no és de moda, sinó de criteri: diccionari per a dades externes, claus dinàmiques i estructures temporals; classe quan hi ha invariants, comportament i un concepte que es repeteix per tot el programa.

Però el que hem construït avui és encara un motlle sense tancar. Una Tasca a la qual s'assignen atributs des de fora pot continuar naixent sense completada i amb la prioritat en "Alta": hem canviat el KeyError per un AttributeError i hem millorat la lectura, poca cosa més. Falta la peça que converteix la classe en una garantia: un bloc que s'executi automàticament en crear cada instància, que exigeixi les dades imprescindibles, les validi contra PRIORITATS i EQUIP, i que a més permeti desar juntament amb les dades les funcions que avui van soltes per tascafacil.py. A Atributs, mètodes i constructor arriba __init__, s'explica d'una vegada què és aquell self que apareix a tots els exemples d'internet, i TascaFàcil fa el salt a la v0.15: una classe Tasca de veritat, amb validació al constructor i mètodes propis, i un programa que deixa de manejar diccionaris per manejar objectes.

© Copyright 2026. Tots els drets reservats