La lliçó anterior va acabar amb dos caps solts. El primer: l'agenda de TascaFàcil v0.15 és una llista pelada a la qual qualsevol part del programa pot afegir el que li vingui de gust —un objecte Tasca, un diccionari solt o un número—, i les operacions que la manegen continuen repartides pel fitxer. El segon: desar_tasques està trencat, perquè json.dump no sap escriure objectes. Aquesta lliçó resol tots dos, i de passada t'ensenya a treballar còmodament amb molts objectes alhora, que és el que fa un programa real gairebé tota l'estona. Veuràs com recórrer, filtrar, comptar, ordenar i cercar objectes aprofitant tot el que has après als mòduls 5 i 6, i després faràs el mateix salt de la lliçó anterior però un nivell més amunt: crear una classe Agenda que contingui les tasques i ofereixi les operacions que avui van soltes. Al final apareixeran, en la seva versió més bàsica, l'herència i el polimorfisme.
Contingut
- Llista d'objectes enfront de llista de diccionaris
- Recórrer, filtrar i comptar objectes
- Ordenar objectes
- Cercar objectes i construir índexs
- Composició: la classe
Agenda - Per què l'agenda ha de protegir la seva llista interna
- Serialitzar i reconstruir:
to_dictifrom_dict - Herència i polimorfisme, el bàsic
- TascaFàcil v0.16
- Errors comuns i consells
- Exercicis
- Conclusió
- Llista d'objectes enfront de llista de diccionaris
L'estructura és la mateixa que al mòdul 5 —una llista— però el que hi ha a dins ha canviat:
agenda = [
Tasca("Cartell fira del llibre", "Luis", "alta", 3),
Tasca("Logotip Forn Sole", "Nuria", "alta", 5),
Tasca("Pressupost client Vidal", "Marta", "mitjana", 2),
]
print(len(agenda), agenda[0].titol) # 3 Cartell fira del llibreTot el que saps de llistes continua valent: índexs, talls, append, len, in, comprensions i sorted. L'única cosa que canvia és com s'accedeix a cada camp, i aquí hi ha el guany:
| Llista de diccionaris | Llista d'objectes | |
|---|---|---|
| Accés a un camp | t["titol"] |
t.titol |
| Nom mal escrit | t["titolp"] → KeyError en executar |
t.titolp → l'editor ho subratlla en escriure |
| Autocompletat de l'editor | Cap: les claus són text | Sí: en prémer . apareixen els atributs |
| Camps garantits i comportament | Cap garantia; funcions soltes | Els del constructor; mètodes de l'objecte |
La segona fila és la que més es nota en el dia a dia: amb diccionaris, tasca["responsble"] és un text perfectament vàlid per a Python i la falla no apareix fins que aquella línia s'executa; amb objectes, tasca.responsble és un nom que l'editor no reconeix i te'l marca en vermell mentre escrius. És la diferència entre trobar un error en dos segons o en dues setmanes.
- Recórrer, filtrar i comptar objectes
Els tres patrons del mòdul 5 es tradueixen directament: recórrer amb for, filtrar amb comprensió i comptar amb sum.
for tasca in agenda: # recorrer
print(tasca) # fa servir __str__
pendents = [t for t in agenda if not t.completada] # filtrar
quantes = sum(1 for t in agenda if not t.completada) # comptar
dies = sum(t.dies for t in agenda if not t.completada) # sumar
print(f"{quantes} tasques pendents, {dies} dies de feina.")Tres apunts sobre aquestes línies, perquè condensen mig curs:
sum(1 for t in agenda if ...)compta sense construir cap llista intermèdia: el generador va produint uns isumels acumula. Equival alen([t for t in agenda if ...])sense gastar memòria, i és la forma idiomàtica de comptar amb condició.- El filtre pot fer servir mètodes, no només atributs:
[t for t in agenda if t.esta_endarrerida()]és vàlid i molt més expressiu que repetir la comparació de dies a cada lloc. Aquest és l'avantatge d'haver ficat el comportament dins de la classe. - Els objectes filtrats no es copien.
pendentsconté referències a les mateixes tasques, així quependents[0].completar()afecta també la tasca que hi ha aagenda: és l'aliasing de 05-01, ara amb objectes.
- Ordenar objectes
A 06-02 vas aprendre a ordenar amb sorted(..., key=...). Amb objectes, la clau s'escriu amb la notació del punt:
from operator import attrgetter
per_dies = sorted(agenda, key=lambda t: t.dies) # una lambda
per_dies = sorted(agenda, key=attrgetter("dies")) # equivalent
per_persona = sorted(agenda, key=attrgetter("responsable", "dies")) # tupla-clau
ORDRE_PRIORITAT = {"alta": 0, "mitjana": 1, "baixa": 2}
per_urgencia = sorted(agenda, key=lambda t: (ORDRE_PRIORITAT[t.prioritat], t.dies))operator.attrgetter és a l'atribut el que itemgetter era a la clau del diccionari: attrgetter("dies") retorna una funció que, donat un objecte, n'extreu l'atribut dies. Amb attrgetter("responsable", "dies") s'obté directament la tupla-clau de dos criteris. I l'última línia recupera la tècnica de 06-02: una tupla ordena primer pel seu primer element i, en cas d'empat, pel segon. Encara hi ha una alternativa més: ensenyar a la mateixa classe com comparar-se, definint el mètode especial __lt__ (de less than, «menor que»).
def __lt__(self, altra):
"""Ordre natural: primer la mes urgent; a igual urgencia, la mes curta."""
return ((ORDRE_PRIORITAT[self.prioritat], self.dies)
< (ORDRE_PRIORITAT[altra.prioritat], altra.dies))
print(sorted(agenda)[0].titol) # sense key: fa servir __lt__
print(min(agenda).titol) # min i max tambe el fan servir| Forma d'ordenar | Quan fer-la servir |
|---|---|
key=lambda t: ... o key=attrgetter("camp") |
Ordenacions puntuals; la segona, més llegible |
__lt__ a la classe |
Quan el tipus té un ordre natural evident |
La regla pràctica és aquesta: defineix __lt__ només si existeix un ordre que qualsevol donaria per descomptat per a aquell objecte (una data, un número de factura, la urgència d'una tasca); si hi ha diversos ordres igual de raonables, és millor key, perquè deixa explícit a cada crida quin criteri es fa servir.
- Cercar objectes i construir índexs
La cerca lineal de 06-01 té en Python una forma abreujada molt còmoda: next() sobre un generador.
def cercar(agenda, titol):
"""Retorna la primera tasca amb aquest titol, o None si no n hi ha cap."""
return next((t for t in agenda if t.titol.lower() == titol.lower()), None)
trobada = cercar(agenda, "logotip forn sole")
print(trobada.responsable if trobada else "No trobada") # Nurianext(generador, valor_per_defecte) demana el primer element que produeixi el generador i, si no n'hi ha cap, retorna el valor per defecte en lloc de fallar. Com que el generador és mandrós, deixa de recórrer tan bon punt troba la primera coincidència: és exactament la cerca lineal amb sortida primerenca, escrita en una línia. El segon argument no és opcional a la pràctica: sense ell, un generador buit llança StopIteration. I quan una consulta es repeteix molt, es construeix un índex, igual que a 06-01, però ara de responsable a llista d'objectes:
index = {}
for tasca in agenda:
index.setdefault(tasca.responsable, []).append(tasca) # responsable -> [Tasca, ...]
print([t.titol for t in index.get("Nuria", [])]) # get: [] si no en te capEl compromís és el mateix d'aleshores: construir l'índex costa O(n) una vegada i després cada consulta és O(1), però l'índex queda obsolet tan bon punt s'afegeix o s'esborra una tasca. A la classe Agenda de la secció següent això deixa de ser un problema, perquè la mateixa agenda pot reconstruir-lo quan canvia.
- Composició: la classe
Agenda
AgendaTotes les funcions anteriors reben agenda com a primer paràmetre, i aquest és exactament el senyal que vam reconèixer a 07-02: un munt de funcions que giren al voltant de la mateixa dada volen ser una classe. L'Agenda no és una llista: té una llista. Aquesta relació —un objecte que en conté d'altres— s'anomena composició, i és la manera més comuna i més sana de combinar classes.
classDiagram
class Agenda {
-_tasques: list
+afegir(tasca)
+eliminar(titol)
+cercar(titol)
+filtrar_per(criteri)
+ordenades(clau)
}
class Tasca {
+titol
+completar()
+to_dict()
}
Agenda "1" o-- "0..*" Tasca : conte
class Agenda:
"""Colleccio de tasques de l'estudi, amb les operacions que li son propies."""
def __init__(self, tasques=None):
self._tasques = list(tasques) if tasques else [] # copia el que rep
def __len__(self):
return len(self._tasques) # habilita len(agenda)
def __iter__(self):
return iter(self._tasques) # habilita: for t in agenda
def afegir(self, tasca): # rebutja el que no sigui Tasca
if not isinstance(tasca, Tasca) or tasca in self._tasques: # i els duplicats
return False
self._tasques.append(tasca)
return True
def eliminar(self, titol): # retorna si ha esborrat res
tasca = self.cercar(titol)
if tasca is not None:
self._tasques.remove(tasca) # remove fa servir __eq__ per dins
return tasca is not None
def cercar(self, titol): # primera amb aquest titol, o None
return next((t for t in self._tasques if t.titol.lower() == titol.lower()), None)
def filtrar_per(self, criteri): # criteri: funcio que dona bool
return [t for t in self._tasques if criteri(t)]
def ordenades(self, clau=None):
"""Copia ordenada; per ordre natural (__lt__) si no es dona clau."""
return sorted(self._tasques) if clau is None else sorted(self._tasques, key=clau)
def resum_per_responsable(self):
"""Diccionari responsable -> {'tasques': n, 'dies': d} del que resta pendent."""
resum = {}
for t in self._tasques:
if not t.completada:
dades = resum.setdefault(t.responsable, {"tasques": 0, "dies": 0})
dades["tasques"], dades["dies"] = dades["tasques"] + 1, dades["dies"] + t.dies
return resumEl que s'hi guanya és enorme, i convé veure-ho en ús:
agenda = Agenda()
agenda.afegir(Tasca("Cartell fira del llibre", "Luis", "alta", 3))
agenda.afegir(Tasca("Logotip Forn Sole", "Nuria", "alta", 5))
agenda.afegir("una cadena qualsevol") # False: no es una Tasca
print(len(agenda)) # 2, gracies a __len__
for tasca in agenda: print(tasca) # gracies a __iter__
print(agenda.resum_per_responsable())
# {'Luis': {'tasques': 1, 'dies': 3}, 'Nuria': {'tasques': 1, 'dies': 5}}Els dos mètodes especials són els que fan que l'agenda es comporti com una col·lecció de Python: __len__ habilita len(agenda) i també que if agenda: sigui fals quan està buida; __iter__ habilita for tasca in agenda, i amb ell funcionen de propina in, list(agenda), sum(...) i les comprensions. Fixa't a més que afegir valida, igual que feia el constructor de Tasca: rebutja el que no sigui una tasca i evita duplicats aprofitant l'__eq__ que vam definir a 07-02, perquè l'operador in el fa servir per dins. El forat pel qual s'esmunyia un diccionari solt està tancat.
- Per què l'agenda ha de protegir la seva llista interna
L'atribut es diu _tasques amb un guió baix al davant. És una convenció de Python, no una prohibició: significa «això és interior de la classe, no ho toquis des de fora». Python no ho impedeix, però qualsevol programador que ho llegeixi sap que allà no hi ha de ficar mà. Per què importa? Perquè si l'agenda lliura la seva llista real, qualsevol es pot saltar totes les seves regles:
def tasques(self):
return self._tasques # MALAMENT: lliura la llista de veritat
llista = agenda.tasques()
llista.append("aixo no es una tasca") # el control d afegir() s ha esfumat
llista.clear() # i l agenda s ha quedat buidaÉs l'aliasing de 05-01 en la seva forma més perillosa: llista i agenda._tasques són el mateix objecte, i qui tingui l'una té l'altra. La solució és retornar una còpia, amb return list(self._tasques) en lloc de return self._tasques. Convé precisar fins on arriba aquesta protecció, perquè és una còpia superficial (05-04): la llista és nova, però les tasques de dins són les mateixes. Afegir o treure elements de la còpia ja no afecta l'agenda; en canvi, copia[0].completar() sí que modifica la tasca original. I està bé que sigui així: normalment vols protegir l'estructura, no impedir que es treballi amb les tasques. Si necessitessis aïllament total, aquí entraria copy.deepcopy, amb el seu cost. Observa que ordenades i filtrar_per ja compleixen la regla sense esforç: sorted i les comprensions sempre construeixen llistes noves.
- Serialitzar i reconstruir:
to_dict i from_dict
to_dict i from_dictArribem al segon cap solt. En intentar json.dump(agenda, fitxer) amb objectes a dins, Python respon TypeError: Object of type Tasca is not JSON serializable. El motiu és que json només sap escriure tipus bàsics —diccionaris, llistes, cadenes, números, booleans i None— i no té manera d'endevinar quina part d'un objecte cal desar. Així que la conversió la fem nosaltres, amb un mètode a l'anada i un constructor alternatiu a la tornada:
def to_dict(self): # a la classe Tasca
"""Retorna la tasca com a diccionari, a punt per al JSON."""
return {"titol": self.titol, "responsable": self.responsable,
"prioritat": self.prioritat, "dies": self.dies,
"fets": self.fets, "completada": self.completada}
@classmethod
def from_dict(cls, dades):
"""Crea una Tasca a partir d un diccionari llegit del JSON."""
... # el des_de_diccionari de 07-02, amb el seu nom definitiuto_dict recorre els atributs que cal persistir i retorna el diccionari; from_dict fa el camí invers passant pel constructor. Amb aquestes dues peces, l'agenda recupera la persistència:
def desar(self, ruta=RUTA_JSON): # a la classe Agenda
"""Desa totes les tasques al fitxer JSON."""
with open(ruta, "w", encoding="utf-8") as f:
json.dump([t.to_dict() for t in self._tasques], f, indent=2, ensure_ascii=False)
@classmethod
def carregar(cls, ruta=RUTA_JSON):
"""Retorna l'Agenda desada, o una de buida si no hi ha fitxer."""
if not ruta.exists():
return cls() # agenda buida: primera execucio
with open(ruta, "r", encoding="utf-8") as f:
return cls(Tasca.from_dict(d) for d in json.load(f))Tres detalls que mereixen atenció. La comprensió [t.to_dict() for t in self._tasques] converteix la llista sencera d'objectes a llista de diccionaris en una línia; el JSON resultant és idèntic al de la v0.14, així que els fitxers antics continuen servint. carregar és un @classmethod perquè fabrica una agenda, igual que from_dict fabrica una tasca: es crida Agenda.carregar(), sense tenir cap agenda abans. I la reconstrucció passa cada diccionari per Tasca.from_dict, és a dir, pel constructor: les dades brutes que hi hagués al fitxer —una prioritat "Alta", un camp inventat que s'ignora— es normalitzen en entrar. El programa deixa d'heretar la brossa del seu propi historial.
- Herència i polimorfisme, el bàsic
La Marta necessita una cosa nova: tasques que es repeteixen cada cert temps, com el manteniment mensual del web del Forn Solé. Són tasques normals més una periodicitat. L'herència permet dir exactament això: crear una classe a partir d'una altra, quedant-se amb tot el que la primera ja sap fer.
class TascaRecurrent(Tasca): # entre parentesis, la classe base
"""Una tasca que es repeteix cada cert nombre de dies."""
def __init__(self, titol, responsable, prioritat="mitjana", dies=1, cada_dies=30):
super().__init__(titol, responsable, prioritat, dies) # el constructor de Tasca
self.cada_dies = cada_dies if cada_dies > 0 else 30
def __str__(self):
return f"{super().__str__()} (cada {self.cada_dies}d)" # reutilitza i afegeixmanteniment = TascaRecurrent("Manteniment web Sole", "Luis", "mitjana", 1, 30)
print(manteniment.completar()) # True: metode heretat, no l hem escrit
print(manteniment.responsable, isinstance(manteniment, Tasca)) # Luis TrueLes tres idees imprescindibles:
class Filla(Mare)hereta tots els atributs i mètodes de la classe base:completar,reassignar,to_dicto__eq__funcionen sense escriure ni una línia.super()dona accés a la classe base.super().__init__(...)executa el constructor deTasca—amb tota la seva validació— i després la filla hi afegeix el que és seu; oblidar aquesta crida és l'error clàssic, i deixa la tasca sensetitol, senseresponsablei sense res. Sobreescriure un mètode és tornar-lo a definir a la filla, com fa__str__, que tot i així reaprofita el de la mare ambsuper().__str__().
I ara el polimorfisme, que sona grandiloqüent i és d'una senzillesa desarmant:
mixta = [Tasca("Cartell fira del llibre", "Luis", "alta", 3),
TascaRecurrent("Manteniment web Sole", "Luis", "mitjana", 1, 30)]
for t in mixta:
print(t) # cada objecte fa servir LA SEVA versio de __str__El bucle no pregunta de quina classe és cada element ni té ni un sol if: cada objecte sap imprimir-se a la seva manera. Això és el polimorfisme, funciona igual amb to_dict o completar, i és el que permetrà afegir demà una TascaUrgent sense tocar el codi que recorre l'agenda. Dit això, l'herència es fa servir molt menys del que sembla:
Herència (TascaRecurrent(Tasca)) |
Composició (Agenda té tasques) |
|
|---|---|---|
| Relació | «és un»: una tasca recurrent és una tasca | «té un»: una agenda té tasques |
| Acoblament | Fort: un canvi a la mare afecta les filles | Feble: cada classe evoluciona sola |
| Freqüència real | Poca | Gairebé sempre |
La regla pràctica és preferir composició, i reservar l'herència per a quan la frase «una X és una Y» sigui certa sense forçar-la i la filla no necessiti desfer res del que fa la mare. Agenda no hereta de list precisament per això: una agenda no és una llista —no té sentit agenda.sort(reverse=True) ni agenda + [3, 4]—, sinó que en té una.
- TascaFàcil v0.16
Es junta tot. El programa ja no maneja una llista solta, sinó un objecte Agenda, i les funcions que la recorrien s'han mudat a dins:
# tascafacil.py - Estudi Alba / Versio 0.16: l'agenda es un objecte
# --- Tasca (v0.15) amb to_dict/from_dict, TascaRecurrent i Agenda: seccions 5 a 8 ---
def mostrar_llistat(agenda):
"""Mostra l'agenda ordenada per urgencia."""
if not agenda: # __len__ ho fa possible
print("L'agenda es buida.")
return
for numero, tasca in enumerate(agenda.ordenades(), start=1):
print(f"{numero:>2}. {tasca}")
fetes = sum(1 for t in agenda if t.completada) # __iter__ ho fa possible
print(f"Total: {len(agenda)} tasques, {fetes} completades.")
def main():
"""Executa el bucle principal de l'aplicacio."""
agenda = Agenda.carregar() # @classmethod: fabrica l'agenda
while True:
mostrar_menu()
opcio = demanar_opcio("Tria una opcio (1-10): ", OPCIONS)
if opcio == "10" and confirmar("Segur que vols sortir?"):
agenda.desar()
break
# ... les altres opcions criden agenda.afegir(...), agenda.cercar(...),
# agenda.filtrar_per(...), agenda.eliminar(...) i els metodes de TascaCompara l'abans i el després d'una operació qualsevol:
| A la v0.15 | A la v0.16 |
|---|---|
agenda.append(tasca) sense comprovar res |
agenda.afegir(tasca), que valida i evita duplicats |
cercar_si(agenda, criteri) |
agenda.cercar(titol) / agenda.filtrar_per(criteri) |
sorted(agenda, key=clau_ordre) a tres llocs |
agenda.ordenades() |
desar_tasques(agenda) trencat pels objectes |
agenda.desar() amb to_dict |
El programa principal ha aprimat fins a quedar-se en el que li pertoca: parlar amb l'usuari. Tota la lògica de l'agenda viu a Agenda, i tota la d'una tasca, a Tasca.
Errors Comuns i Consells
- Retornar la llista interna en comptes d'una còpia. És l'error de la secció 6 i anul·la la validació de la classe:
return list(self._tasques), sempre. - Confondre
_tasquesamb una cosa privada de veritat. El guió baix és un senyal per a humans:agenda._tasques.append(3)funciona igual, i la disciplina la posa l'equip, no el llenguatge. - Heretar de
listodictper «estalviar-se» la composició. Arrossega desenes de mètodes que no vols (sort,pop,extend) saltant-se les teves regles. Conté la llista dins de la teva classe. - Oblidar
super().__init__(...)al constructor de la filla: els atributs de la mare no es creen i tot falla després amb unAttributeError. - Fer servir
next(...)sense valor per defecte.next(t for t in agenda if ...)llançaStopIterationquan no hi ha coincidències: posa-hi sempre el, None. I defineix__iter__i__len__en parella: amb tots dos, la teva classe es comporta com una col·lecció de Python en gairebé qualsevol context. - Consell: mesura l'índex abans de construir-lo. Un
index_per_responsablesobre una agenda de vint tasques no estalvia res i cal mantenir-lo al dia; amb vint mil, canvia el programa. És la regla de 06-04: mesurar abans d'optimitzar.
Exercicis
Exercici 1: Consultes sobre una llista d'objectes
Amb una llista d'objectes Tasca, escriu expressions d'una línia que retornin: (a) els títols de les pendents de la Nuria; (b) quantes de prioritat alta queden sense completar; (c) la tasca amb més dies de feina; (d) la llista ordenada per responsable i, dins de cada persona, per dies descendent.
Exercici 2: Ampliar la classe Agenda
Afegeix a Agenda tres mètodes: pendents(), amb les tasques sense completar; dies_pendents(), amb el total de dies de feina que queden; i __contains__(self, titol), que permeti escriure "Cartell fira del llibre" in agenda. Justifica per què pendents() no ha de retornar self._tasques.
Exercici 3: Una subclasse amb super()
Crea TascaAmbClient(Tasca), que afegeixi l'atribut client (text obligatori), sobreescrigui __str__ per mostrar-lo entre claudàtors al final i ampliï to_dict() perquè el diccionari inclogui també el client. Comprova després que una llista mixta de Tasca i TascaAmbClient s'imprimeix i es desa correctament sense ni un sol if.
Solucions
Solució 1.
[t.titol for t in agenda if t.responsable == "Nuria" and not t.completada] # (a)
sum(1 for t in agenda if t.prioritat == "alta" and not t.completada) # (b)
max(agenda, key=attrgetter("dies")) # (c) max amb key, com sorted
sorted(agenda, key=lambda t: (t.responsable, -t.dies)) # (d)L'apartat (d) té el truc de 06-02: com que reverse=True invertiria els dos criteris, es nega només el numèric amb -t.dies. Amb textos no es pot, i caldria ordenar dues vegades aprofitant que Timsort és estable: primer pel criteri secundari i després pel principal.
Solució 2.
def pendents(self):
"""Llista nova amb les tasques sense completar."""
return [t for t in self._tasques if not t.completada]
def dies_pendents(self):
"""Dies de feina que queden per fer a tota l'agenda."""
return sum(t.dies for t in self.pendents())
def __contains__(self, titol): # 'Cartell fira del llibre' in agenda
return self.cercar(titol) is not Nonependents() no pot retornar self._tasques per dos motius: seria incorrecte —inclouria les completades— i, més de fons, lliuraria la llista interna, de manera que qui la rebés podria buidar-la o omplir-la de brossa saltant-se afegir. La comprensió resol totes dues coses, perquè construeix una llista nova. I __contains__ és un altre mètode especial de la família de __len__ i __iter__: sense ell, in funcionaria recorrent l'__iter__ i comparant objectes, però així podem cercar per títol, que és el natural aquí.
Solució 3.
class TascaAmbClient(Tasca):
"""Una tasca associada a un client concret de l'estudi."""
def __init__(self, titol, responsable, client, prioritat="mitjana", dies=1):
super().__init__(titol, responsable, prioritat, dies)
self.client = client.strip() if client.strip() else "Sense client"
def __str__(self):
return f"{super().__str__()} [{self.client}]"
def to_dict(self):
dades = super().to_dict() # el diccionari de la classe base
dades["client"] = self.client # i hi afegim el que es nostre
return dades
mixta = [Tasca("Cartell fira del llibre", "Luis", "alta", 3),
TascaAmbClient("Logotip", "Nuria", "Forn Sole", "alta", 5)]
for t in mixta: print(t) # cadascuna fa servir el seu __str__El patró dades = super().to_dict() i després afegir-hi és el mateix de __str__: reutilitzar el que la classe base ja fa bé i ampliar-ho, en lloc de copiar els seus sis camps i arriscar-se que es desincronitzin el dia que Tasca en guanyi un de nou. I el bucle final demostra el polimorfisme: no hi ha ni un isinstance ni un if, i cada objecte es comporta com li correspon.
Conclusió
Treballar amb llistes d'objectes és igual que treballar amb llistes de diccionaris, tret que l'accés és t.titol en comptes de t["titol"], l'editor autocompleta i avisa dels noms mal escrits, i els camps estan garantits pel constructor. Els patrons de sempre es tradueixen sense esforç: recórrer amb for, filtrar amb comprensions —fins i tot cridant mètodes com t.esta_endarrerida()—, comptar amb sum(1 for t in ... if ...), ordenar amb key=lambda t: t.dies o attrgetter, combinar criteris amb tupla-clau i, si el tipus té un ordre natural evident, definir __lt__ perquè sorted, min i max funcionin sense key. Cercar és next((t for t in agenda if ...), None), i quan una consulta es repeteix molt, un índex de responsable a llista d'objectes. Tot això, agrupat al voltant de la mateixa dada, demanava a crits una classe: l'Agenda, que per composició conté la llista i exposa afegir —que valida i evita duplicats fent servir __eq__—, eliminar, cercar, filtrar_per, ordenades i resum_per_responsable, més __len__ i __iter__ perquè es comporti com una col·lecció de Python. La regla que sosté tota aquesta protecció és no lliurar mai la llista interna, sinó una còpia. I com que json.dump no sap escriure objectes, l'anada i la tornada es fan a mà amb to_dict() i el @classmethod from_dict(), cosa que a més neteja les dades antigues en passar-les pel constructor. Finalment has vist l'herència en la seva forma mínima —TascaRecurrent(Tasca), super().__init__(...), sobreescriure __str__— i el polimorfisme, que permet recórrer una llista mixta sense ni un sol if; amb l'advertiment que a la pràctica es prefereix la composició, i que l'herència es reserva per a quan «una X és una Y» sigui cert sense forçar-ho.
TascaFàcil és la v0.16: Agenda.carregar() en arrencar, agenda.desar() en sortir, i un main() que ja només s'ocupa de parlar amb l'usuari. Però el projecte té ara un problema de mida: tascafacil.py acumula tres classes, una dotzena de funcions d'entrada i sortida, les constants, la persistència i el menú, tot en un únic fitxer que ja costa de recórrer per trobar on tocar. Els programes de veritat no s'escriuen així. A Mòduls, paquets i importacions veurem com repartir el projecte en diversos fitxers que s'importen entre si, què passa exactament quan Python importa alguna cosa —i per fi, l'explicació completa de l'if __name__ == "__main__": que arrosseguem des de 04-04—, com s'organitza un paquet, què porta la biblioteca estàndard i com s'instal·len llibreries de tercers. TascaFàcil deixarà de ser un fitxer per convertir-se en el paquet tascafacil/.
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ó
