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
- L'agenda fràgil: una demostració incòmoda
- Què és un objecte: dades i comportament junts
- Classe i instància: el motlle i els exemplars
- La sintaxi mínima:
classi creació d'instàncies - Atributs assignats des de fora (i per què encara no n'hi ha prou)
type(),isinstance()i «en Python tot és un objecte»- Atributs de classe enfront d'atributs d'instància
- L'espai de noms de l'objecte:
__dict__,getattr,setattrihasattr - Diccionari o objecte: quan compensa cadascun
- Els quatre pilars de la POO, en una taula
- Errors comuns i consells
- Exercicis
- Conclusió
- 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
KeyErrorsalta amostrar_llistat, però la falla es va cometre molt abans, quan algú va crear aquell diccionari sensecompletada. 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, quanclau_ordrefaciORDRE_PRIORITAT["Alta"], o quan un filtretasca["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.
- 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.
- 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).
- La sintaxi mínima:
class i creació d'instàncies
class i creació d'instànciesLa 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 diferentsTres 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 undefo en unif. - El nom de les classes s'escriu per conveni en
CamelCase:Tasca,TascaRecurrent,Agenda. Les funcions i les variables continuen ensnake_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__.
- 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.
type(), isinstance() i «en Python tot és un objecte»
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 tipusLa 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.
- 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 tocatUna 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 tascaCom 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) |
- L'espai de noms de l'objecte:
__dict__, getattr, setattr i hasattr
__dict__, getattr, setattr i hasattrPer 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-hoAixò 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.
- 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.
- 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 escriuscartell = Tasca(sense parèntesis),cartellno és una tasca: és la classe mateixa, icartell.titol = "..."modificaria el motlle per a tot el programa. Comprova-ho ambtype(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.comptadorcreient 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 aTasca.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, noTasquesniGestorDeTasques), enCamelCase. 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__, preguntaisinstance. 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 2Client.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.
Fonaments de la Programació
Mòdul 1: Introducció a la Programació
- Què és la programació?
- Història de la programació
- Llenguatges de programació
- Entorns de desenvolupament
- Del problema a l'algorisme
Mòdul 2: Conceptes Bàsics
- Variables i tipus de dades
- Operadors i expressions
- Entrada i sortida de dades
- Conversió de tipus i validació de dades
Mòdul 3: Estructures de Control
Mòdul 4: Funcions i Procediments
- Definició i ús de funcions
- Paràmetres i retorn de valors
- Àmbit de variables
- Descompondre un programa en funcions
- Funcions com a valors: lambda i ordre superior
Mòdul 5: Estructures de Dades
- Llistes i arrays
- Cadenes de caràcters
- Diccionaris i conjunts
- Tuples i estructures imbricades
- Desar dades en fitxers: text, CSV i JSON
Mòdul 6: Algorismes Bàsics
Mòdul 7: Objectes i Organització del Codi
- De les dades als objectes: classes i instàncies
- Atributs, mètodes i constructor
- Col·leccions d'objectes
- Mòduls, paquets i importacions
Mòdul 8: Bones Pràctiques i Eines
- Documentació i comentaris
- Depuració i gestió d'errors
- Control de versions
- Proves automatitzades
- Estil, llegibilitat i refactorització
