A De les dades als objectes vam definir una classe Tasca i li vam assignar atributs des de fora, i vam comprovar que allò no arreglava res: continuàvem podent crear una tasca sense completada i amb la prioritat escrita com a "Alta". L'única cosa que va canviar va ser el nom de l'error, de KeyError a AttributeError. Aquesta lliçó aporta la peça que faltava, i és el cor de tot el mòdul. Aquesta peça és el constructor, __init__: un bloc que Python executa automàticament en crear cada instància, i que per tant és l'únic punt del programa pel qual passen, obligatòriament, totes les tasques que arribaran a existir. Allà s'exigeixen les dades imprescindibles i se'n validen els valors. Al seu costat arriben els mètodes, que permeten recollir les funcions soltes que des del mòdul 4 van fent voltes per tascafacil.py i desar-les on pertoquen: dins de la mateixa tasca. Al final de la lliçó, TascaFàcil serà la v0.15 i treballarà amb objectes.
Contingut
__init__: el constructorself, explicat de veritat- Validar dins del constructor
- Mètodes que llegeixen l'estat
- Mètodes que canvien l'estat
__str__enfront de__repr__- Comparar objectes amb
__eq__(i una nota sobre__len__) - Docstrings de classe i de mètode
@property: valors calculats sense duplicar estat@staticmethodi@classmethod@dataclass: la drecera per a classes que només guarden dades- TascaFàcil v0.15: la classe
Tasca - Errors comuns i consells
- Exercicis
- Conclusió
__init__: el constructor
__init__: el constructorEl constructor és un mètode especial anomenat __init__ (dos guions baixos a cada costat) que Python executa just després de crear una instància. La seva feina és deixar l'objecte a punt per fer-se servir: donar valor a tots els seus atributs.
class Tasca:
"""Una tasca de l'estudi Estudi Alba."""
def __init__(self, titol, responsable, prioritat="mitjana", dies=1):
self.titol = titol
self.responsable = responsable
self.prioritat = prioritat
self.dies = dies
self.completada = False # tota tasca neix pendent
cartell = Tasca("Cartell fira del llibre", "Luis", "alta", 3)
print(cartell.titol, cartell.prioritat, cartell.completada) # ... alta False
Tasca("Pressupost Vidal")
# TypeError: __init__() missing 1 required positional argument: 'responsable'Hi ha diverses coses noves alhora, i convé llegir-les a poc a poc:
__init__no es crida mai a mà. EscriusTasca("Cartell...", "Luis", "alta", 3)i Python crea l'objecte i li passa aquests arguments a__init__per tu. És el mateix mecanisme pel quallist("abc")oint("42")construeixen objectes.- Els paràmetres sense valor per defecte són les dades obligatòries.
titoliresponsableno en tenen, així que és impossible crear una tasca sense ells: l'error salta al lloc i al moment correctes, com mostra l'última línia de l'exemple. Els que sí que en tenen funcionen igual que a les funcions (04-02):prioritat="mitjana"idies=1permeten escriureTasca("Revisar esbossos", "Nuria"), i han d'anar després dels obligatoris. completadano és un paràmetre: es fixa a dins. Cap tasca no neix completada, així que no té sentit preguntar-ho en crear-la. Hi ha atributs que es reben i atributs que es fixen o es calculen, i tots surten del constructor amb valor. Abans, la garantia que una tasca tingués els seus cinc camps depenia que cada programador se'n recordés; ara la garantia és del llenguatge: si l'objecte existeix, els seus atributs existeixen, i l'AttributeErrora mig llistat ha deixat de ser possible.
self, explicat de veritat
self, explicat de veritatself és la paraula que més confon al principi i en realitat no té cap misteri: self és l'objecte sobre el qual s'està treballant. Quan escrius Tasca("Cartell fira del llibre", "Luis"), Python fa dues coses: fabrica un objecte buit i crida __init__ passant-li aquest objecte com a primer argument. Dins del mètode, aquest primer paràmetre s'anomena self per conveni, i self.titol = titol vol dir «desa en aquest objecte el valor rebut».
| El que escrius | El que Python executa realment |
|---|---|
cartell = Tasca("Cartell", "Luis") |
crea l'objecte i crida Tasca.__init__(objecte, "Cartell", "Luis") |
cartell.completar() |
crida Tasca.completar(cartell) |
cartell.reassignar("Nuria") |
crida Tasca.reassignar(cartell, "Nuria") |
D'aquí en surten les dues regles que cal memoritzar: self és sempre el primer paràmetre de tot mètode d'instància, encara que no rebi res més; i self no es passa en cridar, el posa Python a partir de l'objecte que hi ha a l'esquerra del punt. Per això cartell.completar() s'escriu sense arguments encara que la seva definició sigui def completar(self). Dos errors clàssics, amb el seu missatge exacte perquè els reconeguis:
class Tasca:
def __init__(titol, responsable): # MALAMENT: falta self
...
Tasca("Cartell", "Luis")
# TypeError: __init__() takes 2 positional arguments but 3 were givenEl missatge sembla absurd —«n'has donat 3 i n'accepta 2»— fins que recordes que Python afegeix l'objecte com a primer argument. Sense self, els comptes no quadren mai. El segon error no dona cap missatge, i per això és pitjor: escriure dins del constructor titol = titol en lloc de self.titol = titol. Això assigna a una variable local que mor en acabar el mètode (és l'àmbit del mòdul 4 en acció); l'objecte es queda sense titol i la falla apareixerà més tard i en un altre lloc. La regla és simple: tot el que hagi de sobreviure a la crida es desa a self.. Finalment, self no és una paraula reservada —el podries anomenar jo i funcionaria—, però no ho facis: tothom escriu self.
- Validar dins del constructor
Que la tasca tingui els cinc atributs no n'hi ha prou: els seus valors també han de ser correctes. I com que el constructor és el pas obligatori de tota tasca, és el lloc natural per comprovar-ho.
PRIORITATS = ("alta", "mitjana", "baixa")
EQUIP = ("marta", "luis", "nuria")
class Tasca:
def __init__(self, titol, responsable, prioritat="mitjana", dies=1): # v0.15
self.titol = titol.strip() # fora espais sobrants
net = responsable.strip().lower()
self.responsable = net.capitalize() if net in EQUIP else "Marta"
prioritat = prioritat.strip().lower() # 'Alta' -> 'alta'
self.prioritat = prioritat if prioritat in PRIORITATS else "mitjana"
self.dies = dies if 1 <= dies <= 365 else 1
self.completada = False
t = Tasca(" Logotip Forn Sole ", "NURIA", "Alta", 5)
print(f"[{t.titol}] {t.responsable} {t.prioritat} {t.dies}d")
# [Logotip Forn Sole] Nuria alta 5dFixa't en el que acaba de passar: "Alta", el valor que al mòdul 6 anava a fer petar l'ORDRE_PRIORITAT, ja no pot entrar al programa. La classe el normalitza a l'únic punt pel qual passen totes les tasques, i el mateix fa amb els espais del títol i amb "NURIA". Hi ha tres estratègies possibles davant d'un valor invàlid:
| Estratègia | Què fa | Quan convé |
|---|---|---|
| Normalitzar | Arregla el que és arreglable: "Alta" → "alta", treu espais |
Diferències de format, no de contingut |
| Valor segur | Substitueix el que és irrecuperable per un valor per defecte | Dades dubtoses que no han d'aturar el programa |
| Avisar i rebutjar | Impedeix crear l'objecte i comunica la falla | El que és correcte en un programa seriós |
De moment fem servir les dues primeres a consciència. La tercera —llançar un error amb raise i capturar-lo amb try/except— és el contingut de Depuració i gestió d'errors; quan ho estudiïs tornaràs a aquesta classe i substituiràs els valors segurs per errors explícits, sense canviar-ne l'estructura. Mentrestant cal anar amb compte amb un perill: un valor segur silenciós amaga el problema. Si algú escriu Tasca("Cartell", "Pep") i la tasca acaba assignada a Marta sense dir res, l'error existeix però ningú no el veu; com a mínim cal avisar per pantalla amb un print(f"Avis: '{responsable}' no consta a EQUIP; la tasca passa a Marta.") en aquesta branca, i així ho farà la versió definitiva de la secció 12.
- Mètodes que llegeixen l'estat
Un mètode és una funció definida dins de la classe, amb self com a primer paràmetre, que per tant accedeix directament als atributs de l'objecte. N'hi ha de dues famílies, i distingir-les ajuda a dissenyar bé: els que consulten l'estat sense tocar-lo i els que el modifiquen. Comencem pels primers, que recullen lògica fins ara dispersa en funcions soltes (la classe té a més un atribut self.fets = 0, els dies ja invertits):
def esta_endarrerida(self):
"""Indica si s han invertit mes dies dels estimats."""
return not self.completada and self.fets > self.dies
def urgencia(self):
"""Retorna la urgencia real: 'cap', 'critica' o la prioritat."""
if self.completada:
return "cap"
return "critica" if self.esta_endarrerida() else self.prioritat
cartell = Tasca("Cartell fira del llibre", "Luis", "alta", 3)
cartell.fets = 5 # 5 dies invertits de 3 previstos
print(cartell.esta_endarrerida(), cartell.urgencia()) # True criticaTres observacions que valen per a tots els mètodes que escriguis. No reben la tasca com a paràmetre: abans escrivíem esta_endarrerida(tasca) i ara la tasca és self i arriba sola, amb la qual cosa cartell.urgencia() diu qui i què sense ambigüitat. Un mètode pot cridar-ne un altre del mateix objecte, sempre a través de self, com fa urgencia amb self.esta_endarrerida(); sense el self., Python buscaria una funció global amb aquest nom i no la trobaria. I aquests mètodes no imprimeixen: retornen un valor, que és la separació entre entrada/sortida i lògica de 04-04 aplicada dins de les classes; qui crida decideix si l'imprimeix, el suma o el fa servir per filtrar.
- Mètodes que canvien l'estat
L'altra família modifica els atributs de l'objecte. Aquí és on marcar_completada i canviar_prioritat, que des del mòdul 4 rebien un diccionari i el modificaven al lloc, troben per fi el seu espai:
def completar(self):
"""Marca la tasca com a acabada. Retorna False si ja ho estava."""
if self.completada:
return False
self.completada = True
self.fets = max(self.fets, self.dies)
return True
def reassignar(self, persona):
"""Canvia el responsable si la persona pertany a l'equip."""
net = persona.strip().lower()
if net not in EQUIP:
return False
self.responsable = net.capitalize()
return True
def avancar(self, dies_treballats):
"""Suma dies de feina ja invertits en la tasca."""
self.fets += max(0, dies_treballats)
cartell = Tasca("Cartell fira del llibre", "Luis", "alta", 3)
print(cartell.reassignar("nuria"), cartell.reassignar("Pep")) # True False
print(cartell.completar(), cartell.completar()) # True False: la segona no canvia resDues decisions de disseny que val la pena copiar. Retornen True/False per indicar si el canvi s'ha fet, de manera que qui crida avisa l'usuari sense que el mètode imprimeixi res: if not cartell.reassignar(nom): print("Aquesta persona no es a l'equip."). I el mètode protegeix l'invariant: reassignar no admet un responsable fora d'EQUIP, igual que feia el constructor. Aquesta és la regla general de l'encapsulació: si una dada té regles, tots els camins que la modifiquen les han de respectar, i per això es canvia mitjançant mètodes i no escrivint cartell.responsable = "Pep" des de fora.
__str__ enfront de __repr__
__str__ enfront de __repr__Ja vas veure que print(cartell) mostra alguna cosa il·legible: <__main__.Tasca object at 0x7f8b1c0d5e50>. S'arregla amb dos mètodes especials:
def __str__(self): # text per a l usuari final
marca = "[X]" if self.completada else "[ ]"
return f"{marca} {self.titol:<28}{self.responsable:<8}{self.prioritat:<6}{self.dies}d"
def __repr__(self): # text per al programador
return f"Tasca({self.titol!r}, {self.responsable!r}, {self.prioritat!r}, {self.dies})"
cartell = Tasca("Cartell fira del llibre", "Luis", "alta", 3)
print(cartell) # [ ] Cartell fira del llibre Luis alta 3d
print(repr(cartell)) # Tasca('Cartell fira del llibre', 'Luis', 'alta', 3)
print([cartell]) # [Tasca('Cartell fira del llibre', 'Luis', 'alta', 3)]__str__ |
__repr__ |
|
|---|---|---|
| Per a qui | L'usuari del programa | El programador |
| El fan servir | print(obj), str(obj), f-strings |
La consola, repr(obj), els objectes dins de llistes |
| Objectiu | Que es llegeixi bé | Que sigui precís i inequívoc |
| Si només en defineixes un | print funciona, però les llistes continuen lletges |
print també el fa servir com a recanvi |
L'última fila és el consell pràctic: si només n'has d'escriure un, escriu __repr__, perquè Python el fa servir de substitut quan falta __str__ i a més és el que es veu en depurar. La sorpresa més habitual és la de print([cartell]): en imprimir una llista, Python no fa servir el __str__ dels seus elements sinó el seu __repr__, així que una llista de tasques sense __repr__ continua mostrant adreces de memòria. El !r de la f-string, per cert, és la drecera per aplicar repr() a un valor: per això els textos surten entre cometes.
- Comparar objectes amb
__eq__ (i una nota sobre __len__)
__eq__ (i una nota sobre __len__)Per defecte, dos objectes diferents mai no són iguals, encara que continguin les mateixes dades. És coherent amb el que vam veure a 05-01 sobre is i ==, però gairebé mai no és el que volem. Definint __eq__ decidim nosaltres què significa que dues tasques siguin la mateixa:
def __eq__(self, altra):
"""Dues tasques son la mateixa si coincideixen titol i responsable."""
if not isinstance(altra, Tasca):
return NotImplemented # comparar amb una altra cosa no ens toca
return (self.titol.lower() == altra.titol.lower()
and self.responsable == altra.responsable)
a = Tasca("Cartell fira del llibre", "Luis", "alta", 3)
b = Tasca("cartell fira del llibre", "Luis", "baixa", 9)
print(a == b, a == "Cartell") # True False (sense __eq__, la primera seria False)
print(a in [b]) # True: l'operador 'in' fa servir ==L'interessant és l'última línia: en definir __eq__, operadors i funcions que ja coneixes comencen a funcionar amb els teus objectes. in, .count(), .index() i .remove() sobre llistes fan servir == internament, així que ara localitzen tasques per contingut. Retornar NotImplemented quan l'altre objecte no és una Tasca és la manera educada de dir «jo no sé comparar això»; Python llavors respon False. A la mateixa família hi ha __len__, que defineix què retorna len(obj). En una tasca solta no té sentit —la longitud de què?—, però a la classe Agenda de Col·leccions d'objectes serà evident: len(agenda) ha de donar el nombre de tasques. A aquests mètodes amb guions baixos se'ls anomena mètodes especials o dunder (de double underscore), i la seva gràcia és que connecten les teves classes amb la sintaxi del llenguatge.
- Docstrings de classe i de mètode
Igual que les funcions, les classes i els seus mètodes porten docstring: un text entre triples cometes a la primera línia del cos. Ja l'has vist a tots els exemples d'aquesta lliçó —"""Marca la tasca com a acabada. Retorna False si ja ho estava."""—, amb una diferència: el docstring de la classe sol ocupar diverses línies, perquè a més de dir què representa l'objecte descriu què garanteix, com a """Una tasca de l'estudi. Neix pendent, validada i amb tots els seus camps.""".
Es consulten amb help(Tasca) o Tasca.completar.__doc__, i l'editor els mostra en escriure. La regla mínima: la classe explica què representa i què garanteix; cada mètode, què fa i què retorna. La guia completa és a Documentació i comentaris.
@property: valors calculats sense duplicar estat
@property: valors calculats sense duplicar estatImagina que vols saber quants dies de feina li queden a una tasca. La temptació és desar-ho com un atribut més, self.dies_restants = dies, i aquí comença el problema: cada vegada que algú cridi avancar() o completar() caldrà recordar-se'n de recalcular-lo, i el dia que se n'oblidi l'objecte mostrarà una dada falsa. Això és estat duplicat, una de les fonts d'error més silencioses que hi ha. La solució és no desar-lo, sinó calcular-lo quan es demani. I perquè es llegeixi com un atribut en comptes de com un mètode, es fa servir el decorador @property:
@property
def dies_restants(self):
"""Dies de feina que falten; zero si la tasca ja esta completada."""
return 0 if self.completada else max(0, self.dies - self.fets)
cartell = Tasca("Cartell fira del llibre", "Luis", "alta", 3)
print(cartell.dies_restants) # 3 <- sense parentesis: sembla un atribut
cartell.avancar(2)
print(cartell.dies_restants) # 1 <- sempre coherent: es recalcula
cartell.completar()
print(cartell.dies_restants) # 0 <- i continua sent coherentLa diferència amb un atribut desat és la coherència: tots dos es llegeixen igual, amb obj.x i sense parèntesis, però l'atribut cal actualitzar-lo a mà a cada mètode que l'afecti, mentre que la @property es calcula en el moment i per tant no menteix mai. A canvi paga un petit càlcul per lectura, que tret dels bucles enormes és irrellevant. Els decoradors ja et sonen de @lru_cache (06-03): són funcions que n'embolcallen una altra per modificar-ne el comportament. La regla pràctica: si un valor es pot deduir d'altres atributs, fes-lo @property; si és una dada independent, desa'l. I compte, una @property de només lectura no admet assignació —cartell.dies_restants = 5 dona AttributeError—, cosa que és una protecció, no un inconvenient.
@staticmethod i @classmethod
@staticmethod i @classmethodNo tots els mètodes necessiten un objecte concret. Un @staticmethod és una funció normal que viu dins de la classe per afinitat temàtica: no rep self ni toca cap objecte. Serveix per a utilitats del domini, com comprovar una dada abans de crear res:
@staticmethod
def prioritat_valida(text):
"""Indica si un text es una prioritat acceptable."""
return text.strip().lower() in PRIORITATS
print(Tasca.prioritat_valida("ALTA")) # True: des de la classe, sense instanciaUn @classmethod rep com a primer paràmetre la classe (cls) en comptes de la instància, i el seu ús més valuós és el constructor alternatiu: una altra manera de fabricar objectes a partir de dades amb un format diferent. Just el que necessitem per al JSON de 05-05, on cada tasca està desada com a diccionari:
@classmethod
def des_de_diccionari(cls, dades):
"""Crea una Tasca a partir d un diccionari llegit del JSON."""
tasca = cls(dades["titol"], dades["responsable"],
dades.get("prioritat", "mitjana"), dades.get("dies", 1))
tasca.fets = dades.get("fets", 0)
tasca.completada = dades.get("completada", False)
return tasca
d = {"titol": "Pressupost Vidal", "responsable": "Marta", "prioritat": "Alta"}
print(Tasca.des_de_diccionari(d)) # [ ] Pressupost Vidal Marta alta 1dObserva el detall que ho fa robust: dades.get("prioritat", "mitjana") tolera que la clau falti i el constructor normalitza l'"Alta" que venia del fitxer. És a dir, els diccionaris bruts del mòdul 6 entren, i en surten tasques netes. L'operació inversa —convertir l'objecte en diccionari per poder desar-lo— és to_dict(), i arriba a 07-03 al costat de la classe Agenda, perquè json.dump no sap escriure objectes.
@dataclass: la drecera per a classes que només guarden dades
@dataclass: la drecera per a classes que només guarden dadesQuan una classe es limita a agrupar dades, escriure __init__, __repr__ i __eq__ a mà és pur tràmit. El mòdul dataclasses de la biblioteca estàndard els genera per tu. El mateix Client, abans i després:
class Client: # ABANS: tot a ma
def __init__(self, nom, contacte, actiu=True):
self.nom, self.contacte, self.actiu = nom, contacte, actiu
def __repr__(self): ... # la f-string amb els tres camps
def __eq__(self, altre): ... # isinstance + comparacio camp a camp
from dataclasses import dataclass # DESPRES: el mateix, generat
@dataclass
class Client:
"""Un client de l'estudi."""
nom: str
contacte: str
actiu: bool = True
sole = Client("Forn Sole", "[email protected]")
print(sole, sole == Client("Forn Sole", "[email protected]"))
# Client(nom='Forn Sole', contacte='[email protected]', actiu=True) TrueEl decorador llegeix les línies nom: str —anotacions de tipus, que Python no comprova però que serveixen de documentació i ajuden l'editor— i genera __init__, __repr__ i __eq__. Quan fer servir cada cosa: si la classe només agrupa dades, @dataclass; si hi ha validació, normalització, invariants o comportament propi, classe normal amb __init__ escrit a mà. La nostra Tasca valida, normalitza i decideix, així que es queda com a classe normal; Client, que només transporta dades, és un @dataclass de manual.
- TascaFàcil v0.15: la classe
Tasca
TascaHo apliquem tot. Les funcions soltes que treballaven sobre diccionaris es converteixen en mètodes, i l'agenda passa a ser una llista d'objectes Tasca.
# tascafacil.py - Estudi Alba / Versio 0.15: la tasca es un objecte
CAMPS = ("titol", "responsable", "prioritat", "dies", "fets", "completada")
# --- Resta de constants i funcions d E/S: sense canvis respecte de la v0.14 ---
class Tasca:
"""Una tasca de l'estudi. Neix pendent, validada i amb tots els seus camps."""
# __init__ com a la seccio 3, mes l avis de responsable invalid i
# self.fets = 0; dies_restants (@property), esta_endarrerida, urgencia,
# completar, reassignar, canviar_prioritat, avancar, des_de_diccionari,
# __str__, __repr__ i __eq__ tal com s han escrit en aquesta llico
def clau_ordre(self):
"""Criteri del llistat: prioritat, despres dies, despres titol."""
return (ORDRE_PRIORITAT[self.prioritat], self.dies, self.titol)
def registrar_tasca(agenda):
"""Demana una tasca nova i l afegeix a l agenda."""
agenda.append(Tasca(demanar_text("Titol : "),
demanar_opcio("Responsable : ", EQUIP),
demanar_opcio("Prioritat : ", PRIORITATS),
demanar_enter("Dies (1-365) : ", 1, 365)))
def mostrar_llistat(agenda):
"""Mostra l agenda ordenada per prioritat i dies."""
for numero, tasca in enumerate(sorted(agenda, key=Tasca.clau_ordre), start=1):
print(f"{numero:>2}. {tasca}") # aqui actua __str__Dos detalls nous. Al print del bucle, {tasca} invoca __str__ automàticament: la funció ja no sap res de l'estructura interna d'una tasca. I key=Tasca.clau_ordre funciona perquè un mètode al qual s'accedeix des de la classe és una funció normal que rep l'objecte com a primer argument, així que sorted li passarà cada tasca com a self; també valdria key=lambda t: t.clau_ordre(). Abans i després, línia a línia:
| A la v0.14 (diccionaris) | A la v0.15 (objectes) |
|---|---|
tasca["titol"] |
tasca.titol |
mostrar_fitxa(tasca) |
print(tasca), gràcies a __str__ |
clau_ordre(tasca) / marcar_completada(tasca) |
tasca.clau_ordre() / tasca.completar() |
Validació repartida per registrar_tasca |
Centralitzada a __init__ |
Un KeyError possible en qualsevol llistat |
Impossible: si l'objecte existeix, té els seus camps |
Queda un cap solt evident: desar_tasques ja no funciona. json.dump sap escriure diccionaris, però no objectes Tasca, i protesta amb TypeError: Object of type Tasca is not JSON serializable. La solució —un mètode to_dict() que retorni el diccionari equivalent, amb des_de_diccionari per al camí de tornada— arriba a la propera lliçó.
Errors Comuns i Consells
- Oblidar
selfa la definició del mètode, amb el missatgetakes 1 positional argument but 2 were given. Recorda que Python afegeix l'objecte: si el mètode repnarguments en cridar-lo, la seva definició necessitan + 1paràmetres. I oblidar elself.en assignar dins del constructor no dona cap error: crea una variable local que es perd, així que si un atribut «desapareix», revisa això primer. - Posar un mutable com a valor per defecte d'un paràmetre,
def __init__(self, notes=[]). És el mateix error del mòdul 4 i aquí és pitjor, perquè aquesta llista es comparteix entre totes les instàncies. Fes servirnotes=Nonei a dinsself.notes = notes if notes else []. Tampoc no cal mai cridar__init__a mà (cartell.__init__(...)): escriuTasca(...). - Definir només
__str__i esperar que les llistes es vegin bé.print([tasca])fa servir__repr__: defineix-los tots dos, o com a mínim__repr__. I no modifiquis atributs des de fora saltant-te els mètodes (tasca.prioritat = "Alta"), perquè trenca justament la garantia que dona la classe: si una dada té regles, passa pel mètode. - Consell: posa els mètodes en ordre. Primer
__init__, després les@property, tot seguit els mètodes que consulten, els que modifiquen i al final els especials. Una classe ordenada es llegeix en diagonal.
Exercicis
Exercici 1: La classe Client
Escriu una classe Client el constructor de la qual rebi nom, contacte i dies_credit=30; que normalitzi el nom (sense espais sobrants i amb la primera lletra de cada paraula en majúscula), obligui que dies_credit estigui entre 0 i 90 (si no, 30) i fixi factures = []. Afegeix-hi un mètode facturar(quantitat) que afegeixi la quantitat a la llista si és positiva i retorni True/False, i una @property total_facturat. Prova-la amb el Forn Solé.
Exercici 2: Un mètode a partir d'una funció solta
Aquesta funció treballava amb diccionaris a la v0.14. Converteix-la en un mètode resum() de Tasca sense canviar-ne el comportament i explica per escrit què millora: return f"{tasca['titol']} ({tasca['responsable']}): {estat}, {tasca['dies']} dies", on estat val "completada" o "pendent" segons tasca["completada"].
Exercici 3: @property enfront d'atribut desat
Afegeix a Tasca una @property anomenada percentatge que retorni l'avanç en tant per cent (dies fets sobre dies estimats, amb un màxim de 100, i 100 si està completada). Després respon per escrit: què passaria si en lloc d'una @property es desés self.percentatge al constructor? Escriu la seqüència de crides que demostraria la falla.
Solucions
Solució 1.
class Client:
"""Un client de l'estudi, amb el seu credit i les seves factures."""
def __init__(self, nom, contacte, dies_credit=30):
self.nom = nom.strip().title()
self.contacte = contacte.strip().lower()
self.dies_credit = dies_credit if 0 <= dies_credit <= 90 else 30
self.factures = [] # mutable: sempre creat al constructor
@property
def total_facturat(self):
"""Suma de totes les factures emeses al client."""
return sum(self.factures)
def facturar(self, quantitat):
"""Afegeix una factura si la quantitat es positiva."""
if quantitat <= 0:
return False
self.factures.append(quantitat)
return True
sole = Client(" forn sole ", "[email protected]", 120)
sole.facturar(450.0)
print(sole.nom, sole.dies_credit, sole.facturar(-10), sole.total_facturat)
# Forn Sole 30 False 450.0 <- nom normalitzat, credit segur, rebuigLa clau és a self.factures = [] dins del constructor: si estigués al cos de la classe seria un atribut de classe compartit i les factures d'un client apareixerien als altres (07-01, secció 7). I total_facturat és una @property perquè és una suma deduïble de factures: desar-la obligaria a actualitzar-la a cada facturar.
Solució 2.
def resum(self):
"""Retorna una linia descriptiva de la tasca."""
estat = "completada" if self.completada else "pendent"
return f"{self.titol} ({self.responsable}): {estat}, {self.dies} dies"Tres millores concretes. Viu al costat de les dades: si demà la tasca guanya un camp, el resum és a tres línies de distància i no en un altre punt del fitxer. No pot rebre res que no sigui una tasca: resum_tasca({"titol": "x"}) petava amb KeyError, mentre que tasca.resum() només existeix si tasca és una Tasca. I es llegeix millor al punt d'ús: print(cartell.resum()) diu de qui és el resum sense necessitat de mirar la signatura de cap funció.
Solució 3.
@property
def percentatge(self): # avanc, entre 0 i 100
if self.completada:
return 100
return min(100, round(self.fets / self.dies * 100))
# Si en comptes de la @property es deses self.percentatge = 0 al constructor:
t = Tasca("Cartell fira del llibre", "Luis", "alta", 4)
t.avancar(2) # ha fet la meitat
print(t.percentatge) # 0 <- MENTIDA: ningu no va actualitzar l atributavancar modifica fets, però l'atribut percentatge conserva el valor que li va posar el constructor. És estat duplicat: el mateix fet —quant s'ha avançat— desat en dos llocs que poden discrepar. Amb la @property, el percentatge no existeix fins que es demana i per tant no pot estar desactualitzat. El mateix parany apareixeria amb completar(), que també canvia fets sense tocar percentatge.
Conclusió
El constructor __init__ és el pas obligatori de tots els objectes d'una classe, i per això és on es garanteix que tota tasca neix completa: els paràmetres sense valor per defecte són les dades imprescindibles, els que sí que en tenen cobreixen allò opcional, i els atributs que no es reben —com completada— es fixen a dins. Allà mateix es valida: normalitzar "Alta" a "alta" i comprovar el responsable contra EQUIP elimina d'arrel els KeyError del mòdul 6, i quan estudiïs raise i try/except a 08-02 substituiràs els valors segurs per errors explícits. self no és màgia: és el mateix objecte, que Python passa com a primer argument a partir del que hi ha a l'esquerra del punt, i tot el que hagi de sobreviure a la crida es desa a self.. Els mètodes recullen la lògica que estava solta, distingint els que consulten l'estat (esta_endarrerida, urgencia, clau_ordre) dels que el modifiquen (completar, reassignar, canviar_prioritat), que retornen True/False en comptes d'imprimir. Els mètodes especials connecten la classe amb el llenguatge: __str__ per a l'usuari, __repr__ per al programador —i és el que fan servir les llistes—, __eq__ perquè == i in comparin contingut. @property converteix en atribut de lectura qualsevol valor deduïble dels altres i elimina l'estat duplicat; @staticmethod allotja utilitats sense objecte i @classmethod dona constructors alternatius com Tasca.des_de_diccionari(d), la porta d'entrada de les dades del JSON. I @dataclass estalvia la feina manual a les classes que només agrupen dades.
TascaFàcil és ja la v0.15: l'agenda és una llista d'objectes Tasca, el llistat s'imprimeix amb {tasca} i set funcions soltes s'han mudat dins de la classe. Però fixa't en el que ha quedat despenjat. L'agenda continua sent una llista pelada: qualsevol part del programa pot fer-li append del que vulgui —inclòs un diccionari solt o un número—, les operacions que la manegen (cercar, filtrar, ordenar, resumir) continuen repartides pel fitxer, i desar_tasques està literalment trencat perquè json.dump no sap escriure objectes. A Col·leccions d'objectes farem el mateix salt d'avui, però un nivell més amunt: apareix la classe Agenda, que conté les tasques i ofereix afegir, cercar, filtrar i resum; aprendràs a recórrer, ordenar i indexar objectes, veuràs per què una col·lecció ha de protegir la seva llista interna i recuperaràs la persistència amb to_dict/from_dict per continuar desant en el mateix JSON de 05-05.
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ó
