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

  1. Llista d'objectes enfront de llista de diccionaris
  2. Recórrer, filtrar i comptar objectes
  3. Ordenar objectes
  4. Cercar objectes i construir índexs
  5. Composició: la classe Agenda
  6. Per què l'agenda ha de protegir la seva llista interna
  7. Serialitzar i reconstruir: to_dict i from_dict
  8. Herència i polimorfisme, el bàsic
  9. TascaFàcil v0.16
  10. Errors comuns i consells
  11. Exercicis
  12. Conclusió

  1. 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 llibre

Tot 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.

  1. 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 i sum els acumula. Equival a len([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. pendents conté referències a les mateixes tasques, així que pendents[0].completar() afecta també la tasca que hi ha a agenda: és l'aliasing de 05-01, ara amb objectes.

  1. 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.

  1. 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")   # Nuria

next(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 cap

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

  1. Composició: la classe Agenda

Totes 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: 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 resum

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

  1. 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.

  1. Serialitzar i reconstruir: to_dict i from_dict

Arribem 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 definitiu

to_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.

  1. 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 afegeix
manteniment = 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 True

Les tres idees imprescindibles:

  • class Filla(Mare) hereta tots els atributs i mètodes de la classe base: completar, reassignar, to_dict o __eq__ funcionen sense escriure ni una línia.
  • super() dona accés a la classe base. super().__init__(...) executa el constructor de Tasca —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 sense titol, sense responsable i 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 amb super().__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 una.

  1. 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 Tasca

Compara 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 _tasques amb 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 list o dict per «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 un AttributeError.
  • Fer servir next(...) sense valor per defecte. next(t for t in agenda if ...) llança StopIteration quan 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_responsable sobre 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 None

pendents() 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/.

© Copyright 2026. Tots els drets reservats