La lliçó anterior va acabar assenyalant una esquerda: vendre() llança ValueError per tres motius diferents — unitats invàlides, títol desconegut, estoc insuficient — i qui l'atrapa no pot distingir-los sense llegir el missatge "amb els ulls". Pitjor encara: si el codi de la caixa atrapa except ValueError: per gestionar l'estoc esgotat, atraparà també, per accident, el ValueError d'un bug qualsevol que rondi a prop. La solució és la mateixa que Python fa servir amb si mateix: classes d'excepció pròpies. Igual que FileNotFoundError diu al teu except exactament què ha passat amb el fitxer, EstocInsuficientError dirà a la caixa de Papyrus exactament què ha passat amb la venda — i portarà a dins, com a atributs, el títol, el demanat i el disponible, a punt perquè el codi actuï sense analitzar strings. En aquesta lliçó construirem la jerarquia d'errors de Papyrus, la desarem al seu propi mòdul errors.py i la integrarem a les funcions que ja coneixes.

Contingut

  1. Per què crear excepcions pròpies
  2. La forma mínima: heretar d'Exception
  3. La jerarquia de Papyrus: ErrorPapyrus i les seves filles
  4. Enriquir amb atributs i __str__
  5. Capturar per la base vs capturar per la fulla
  6. obtenir_llibre() vs cercar_llibre(): estricta i tolerant conviuen
  7. Integració a vendre() i el mòdul errors.py

Per què crear excepcions pròpies

Tres raons, de més pràctica a més profunda:

  • L'except de qui crida distingeix el TEU error de la resta. except EstocInsuficientError: atrapa exactament "no queda gènere" i deixa passar qualsevol ValueError accidental (un bug de conversió, una data mal parsejada). Amb excepcions integrades per a tot, la teva gestió d'errors de negoci i els bugs de Python viatgen pel mateix tub.
  • Les dades de l'error viatgen com a atributs, no incrustades en un string. A 07-03, el missatge d'estoc portava "els tres números que importen"; ara el codi podrà llegir e.disponible i oferir a la Júlia els 6 exemplars que sí que queden, en comptes de trossejar un missatge amb els dits creuats.
  • La jerarquia documenta el teu domini. Un cop d'ull a errors.py dirà què pot sortir malament a Papyrus, igual que un cop d'ull a la taula de 07-01 diu què pot sortir malament a Python.

La forma mínima: heretar d'Exception

Una excepció pròpia és una classe normal (mòdul 5) que hereta d'Exception. La versió mínima ni tan sols necessita cos:

class ErrorPapyrus(Exception):
    """Error base de la llibreria Papyrus."""

Amb això ja es pot llançar i atrapar:

raise ErrorPapyrus("alguna cosa va malament a la botiga")
Traceback (most recent call last):
  ...
ErrorPapyrus: alguna cosa va malament a la botiga

Com funciona sense escriure ni un mètode? Herència pura, la del mòdul 5: Exception ja sap rebre un missatge al constructor, guardar-lo, mostrar-se al traceback. La teva subclasse ho hereta tot; de moment només aporta un nom nou que els except poden distingir. Dues regles d'estil universals: heretar d'Exception (no de BaseException — recorda de 07-01 que aquell nivell és per a KeyboardInterrupt i companyia) i acabar el nom en Error quan representa un error, com fan ValueError o FileNotFoundError.

La jerarquia de Papyrus: ErrorPapyrus i les seves filles

Igual que Python penja KeyError i IndexError de LookupError, Papyrus defineix una arrel comuna i d'ella pengen els errors concrets del negoci:

graph TD
    EX[Exception] --> EP[ErrorPapyrus]
    EP --> LNE[LlibreNoTrobatError<br/>atribut: titol]
    EP --> SIE[EstocInsuficientError<br/>atributs: titol, demanat, disponible]
    EP --> SVE[SociInvalidError<br/>atribut: codi]

La base ErrorPapyrus gairebé mai no es llança directament: el seu ofici és agrupar. Gràcies a la regla de coincidència de 07-02 (un except atrapa la classe i les seves subclasses), except ErrorPapyrus: significa "qualsevol error de negoci de la botiga", mentre cada fulla conserva la seva identitat per a qui necessiti afinar.

Enriquir amb atributs i __str__

La versió completa de les tres fulles, amb dades llegibles per màquina:

class ErrorPapyrus(Exception):
    """Error base de la llibreria Papyrus."""


class LlibreNoTrobatError(ErrorPapyrus):
    """S'ha demanat un llibre que no es al cataleg."""

    def __init__(self, titol):
        super().__init__(f"'{titol}' no es al cataleg")
        self.titol = titol


class EstocInsuficientError(ErrorPapyrus):
    """S'han demanat mes unitats de les disponibles."""

    def __init__(self, titol, demanat, disponible):
        super().__init__(titol)  # el missatge detallat el compon __str__
        self.titol = titol
        self.demanat = demanat
        self.disponible = disponible

    def __str__(self):
        return (f"estoc insuficient de '{self.titol}': "
                f"en queden {self.disponible}, se n'han demanat {self.demanat}")


class SociInvalidError(ErrorPapyrus):
    """El codi de soci no existeix o te un format invalid."""

    def __init__(self, codi):
        super().__init__(f"codi de soci invalid: {codi!r}")
        self.codi = codi

Desmuntem les tècniques — totes del mòdul 5, aplicades a un cas nou:

  • __init__ propi amb paràmetres de domini. LlibreNoTrobatError("Rayuela") es construeix amb la dada rellevant, no amb un missatge prefabricat per qui crida. Menys repetició, missatges sempre coherents.
  • super().__init__(missatge) delega en Exception perquè el mecanisme estàndard (el text del traceback, print(e)) continuï funcionant. No l'ometis: una excepció que no crida super().__init__ perd comportaments que altres esperen.
  • Els atributs (self.titol, self.demanat, self.disponible, self.codi) són la càrrega útil: l'except que l'atrapi podrà calcular amb ells.
  • __str__ (el mateix dunder de M5) controla com es mostra l'excepció al traceback i en fer print(e). EstocInsuficientError el fa servir per compondre el missatge a partir dels seus atributs: una única font de veritat, impossible que missatge i dades discrepin.

I així s'aprofita la càrrega útil des de qui crida:

try:
    quantia = vendre(cataleg, "Hamlet", 99)
except EstocInsuficientError as e:
    print(f"Nomes en queden {e.disponible} de '{e.titol}'.")
    if e.disponible > 0:
        resposta = input(f"Vols vendre aquestes {e.disponible}? (s/n) ")

Ni un sol string trossejat: e.disponible és un int a punt per comparar. Això era impossible amb el ValueError de 07-03.

Capturar per la base vs capturar per la fulla

La jerarquia dona a qui crida un dial de precisió. Compara els dos extrems:

# Per la fulla: la caixa vol reaccionar diferent a cada situacio
try:
    quantia = vendre(cataleg, titol, unitats)
except LlibreNoTrobatError as e:
    print(f"No tenim '{e.titol}'. Vols que l'encarreguem?")
except EstocInsuficientError as e:
    print(f"Nomes en queden {e.disponible}; ajustem la venda?")

# Per la base: el menu principal nomes vol que la botiga no caigui
try:
    executar_opcio(opcio)
except ErrorPapyrus as e:
    print(f"Operacio no feta: {e}")
    # i tornem al menu, amb la botiga intacta
Capturar per... Atrapa Quan convé
La fulla (EstocInsuficientError) Només aquella situació exacta Quan cada error té una reacció diferent (la caixa)
La base (ErrorPapyrus) Qualsevol error de negoci de Papyrus, present o futur A la frontera: menú principal, bucle de l'aplicació
Exception Tot, inclosos bugs aliens al negoci Gairebé mai; només a l'última frontera i registrant (07-05)

El matís "o futur" és or: quan al mòdul 10 Papyrus tingui web i aparegui ComandaDuplicadaError(ErrorPapyrus), l'except ErrorPapyrus: del menú el gestionarà sense tocar ni una línia. La jerarquia és un contracte extensible. I continua vigent la regla d'ordre de 07-02: les fulles abans que la base, o la base s'ho menjarà tot i les fulles seran codi mort.

obtenir_llibre() vs cercar_llibre(): estricta i tolerant conviuen

A 07-03 va quedar promesa la germana estricta de cercar_llibre(). Aquí la tens, i la parella completa il·lustra que retornar None i llançar no competeixen: serveixen cridadors diferents.

def cercar_llibre(cataleg, titol):
    """Retorna el Llibre, o None si no hi es. Absencia = resposta legitima."""
    return cataleg.get(titol.strip().casefold())


def obtenir_llibre(cataleg, titol):
    """Retorna el Llibre, o llanca LlibreNoTrobatError si no hi es.

    Per a cridadors que NECESSITEN el llibre: si no hi es, no poden continuar.
    """
    llibre = cercar_llibre(cataleg, titol)
    if llibre is None:
        raise LlibreNoTrobatError(titol)
    return llibre
cercar_llibre() obtenir_llibre()
Si el llibre no hi és Retorna None Llança LlibreNoTrobatError
Pregunta que respon "El teniu?" "Dona-me'l, el necessito"
Cridador típic Consulta de taulell, comprovacions vendre(), reposar(), informes
Obligació de qui crida Comprovar is None Res, o un except on toqui

Fixa't que l'estricta es construeix a sobre de la tolerant: una línia que tradueix absència en excepció. És el mateix patró de la biblioteca estàndard que ja fas servir sense pensar-hi: cataleg["x"] llança KeyError (estricte) i cataleg.get("x") retorna None (tolerant). Papyrus ara parla aquest mateix idioma. El criteri de la taula de 07-03 decideix quina fer servir a cada punt del codi; tenir-les totes dues no és duplicació, és donar a cada cridador el contracte que necessita.

Integració a vendre() i el mòdul errors.py

Primer, la casa de les excepcions. El mòdul 3 va ensenyar a repartir el codi en mòduls; les excepcions pròpies mereixen el seu, perquè les importen tant qui llança com qui atrapa:

papyrus/
├── dades/            (cataleg.json, vendes.csv, socis.json, config.json, ...)
├── copies/
├── informes/
├── errors.py         ← la jerarquia completa: NOMES excepcions
├── models.py         (Llibre, Producte, Soci...)
├── magatzem.py       (carregar/desar cataleg, cercar/obtenir, vendre, reposar)
└── caixa.py          (el programa que l'Anna executa)

errors.py conté les quatre classes de dalt i res més. Així, magatzem.py fa from errors import LlibreNoTrobatError, EstocInsuficientError per llançar-les, i caixa.py importa les que atrapa — sense que la caixa hagi d'importar tot el magatzem només per conèixer-ne els errors, i sense risc d'imports circulars.

I la nova vendre(), amb cada situació parlant el seu propi idioma:

from errors import EstocInsuficientError
# LlibreNoTrobatError surt d'obtenir_llibre(); vendre nomes la deixa passar

def vendre(cataleg, titol, unitats):
    """Descompta estoc i retorna la quantia amb IVA.

    Llanca TypeError/ValueError si els arguments violen el contracte,
    LlibreNoTrobatError si el titol no existeix,
    EstocInsuficientError si no hi ha prou unitats.
    """
    if not isinstance(unitats, int):
        raise TypeError(f"unitats ha de ser int, no {type(unitats).__name__}")
    if unitats <= 0:
        raise ValueError(f"unitats ha de ser positiu, no {unitats}")

    llibre = obtenir_llibre(cataleg, titol)     # llanca LlibreNoTrobatError
    if llibre.estoc < unitats:
        raise EstocInsuficientError(llibre.titol, unitats, llibre.estoc)

    llibre.estoc -= unitats
    return round(llibre.preu * unitats * (1 + IVA_LLIBRES), 2)

Compara-la amb la versió de 07-03 i observa el repartiment final:

  • TypeError/ValueError continuen per als arguments: unes unitats negatives són un error de programació, universal, no un concepte del negoci llibreter. Les integrades expressen això millor que qualsevol classe pròpia.
  • Les situacions del domini — llibre inexistent, estoc curt — fan servir la jerarquia de Papyrus, amb atributs.
  • vendre() s'ha encongit: la guarda d'existència viu ara a obtenir_llibre(), i l'excepció que aquesta llança travessa vendre() netament (la propagació de 07-01 treballant a favor). Cada funció, una responsabilitat.

Amb dades canòniques: vendre(cataleg, "L'Odissea", 9) llança EstocInsuficientError("L'Odissea", 9, 4) — i l'except de la caixa pot oferir els 4 que queden llegint e.disponible. vendre(cataleg, "Rayuela", 1) llança LlibreNoTrobatError amb e.titol == "Rayuela", a punt per al missatge de "l'encarreguem?".

Errors Comuns i Consells

  • Heretar de BaseException. Reservat per al sistema (Ctrl+C, SystemExit). El teu penja d'Exception — normalment de la teva base pròpia.
  • Oblidar super().__init__(...). L'excepció "funciona" però el seu missatge estàndard queda buit o estrany a tracebacks i logs. Sempre delega en la mare.
  • Ficar les dades només a l'string. raise ErrorPapyrus(f"en queden {n}") obliga qui crida a trossejar text. Les dades, a atributs; el text, que el compongui __str__ a partir d'elles.
  • Una classe d'excepció per cada raise. L'extrem oposat a no tenir-ne cap: cinquanta classes que ningú no distingeix. Crea una excepció quan algun except real l'hagi de tractar diferent; si ningú no la distingiria de la seva germana, sobren.
  • Capturar per la base i actuar com si fos una fulla. Dins d'except ErrorPapyrus as e: no existeix e.disponible garantit (podria ser un SociInvalidError). Si necessites els atributs d'una fulla, captura la fulla.
  • Consell: anomena amb la convenció AlgunaCosaError i escriu una docstring d'una línia a cada classe: errors.py es converteix gratis en el catàleg de "què pot sortir malament" del teu sistema.
  • Consell: quan dubtis entre integrada i pròpia, pregunta't qui atraparà. Un except de la teva aplicació amb lògica de negoci? Pròpia. És un error genèric de valors/tipus que qualsevol funció de Python podria llançar? Integrada.

Exercicis

  1. aplicar_descompte_soci(cataleg, titol, codi, socis). Fent servir la jerarquia completa, escriu la funció que retorna el preu amb descompte de soci (DESCOMPTE_SOCI = 0.05, arrodonit a 2 decimals): ha de llançar SociInvalidError si codi no és a la llista socis (fes servir ["LLUIS-001", "MARTA-002", "PAU-003"]), i LlibreNoTrobatError si el títol no existeix (pista: no la llancis tu — deixa que obtenir_llibre() treballi). Comprova que ("Hamlet", "MARTA-002") retorna 9.45 i que "JULIA-999" llança amb e.codi == "JULIA-999".
  2. El taulell que no cau. Escriu un bucle while True de taulell que demani títol i unitats (amb el demanar_enter() de 07-02), cridi vendre() i: davant de LlibreNoTrobatError proposi encarregar-lo; davant d'EstocInsuficientError ofereixi vendre les e.disponible restants si són > 0; davant de qualsevol altre ErrorPapyrus mostri el missatge i continuï; i surti netament quan el títol sigui "fi". Per què l'ordre d'aquests tres except no pot ser cap altre?
  3. Fulla nova sense tocar el menú. Afegeix a errors.py l'excepció PreuInvalidError(ErrorPapyrus) amb atributs titol i preu i un __str__ propi, pensada per a quan una actualització de tarifes porta un preu negatiu. Explica en una frase per què l'except ErrorPapyrus: del menú principal la gestiona sense modificar-se, i quina relació té això amb el polimorfisme del mòdul 5.

Solucions

  1. DESCOMPTE_SOCI = 0.05
    
    def aplicar_descompte_soci(cataleg, titol, codi, socis):
        if codi not in socis:
            raise SociInvalidError(codi)
        llibre = obtenir_llibre(cataleg, titol)   # llanca LlibreNoTrobatError si falta
        return round(llibre.preu * (1 - DESCOMPTE_SOCI), 2)
    

    aplicar_descompte_soci(cataleg, "Hamlet", "MARTA-002", socis)9.45 (els 9.83 de la taula canònica de preus de soci inclouen a més l'IVA del 4 %; aquí només apliquem el descompte). Amb "JULIA-999" salta SociInvalidError: codi de soci invalid: 'JULIA-999' i e.codi val "JULIA-999" — la Júlia i l'Omar són clients, no socis. La guarda del soci va primer: valida l'argument abans de tocar el catàleg, fallar aviat (07-03).

  2. while True:
        titol = input("\nTitol (o 'fi'): ").strip()
        if titol.casefold() == "fi":
            break
        unitats = demanar_enter("Unitats: ")
        try:
            quantia = vendre(cataleg, titol, unitats)
        except LlibreNoTrobatError as e:
            print(f"No tenim '{e.titol}'. Vols que l'encarreguem?")
        except EstocInsuficientError as e:
            if e.disponible > 0:
                print(f"Nomes en queden {e.disponible} de '{e.titol}'. Ajusta la venda.")
            else:
                print(f"'{e.titol}' esta esgotat.")
        except ErrorPapyrus as e:
            print(f"Operacio no feta: {e}")
        else:
            print(f"Venda feta: {quantia:.2f} € (amb IVA)")
    

    L'ordre és obligatori per la regla de 07-02 aplicada a la nostra jerarquia: ErrorPapyrus és la mare de les altres dues; si anés primer, ho atraparia tot i les fulles serien codi mort. De l'específic al general — ara amb classes que has dissenyat tu. L'else manté el missatge d'èxit fora del try, com mana 07-02.

  3. class PreuInvalidError(ErrorPapyrus):
        """Una actualitzacio de tarifes porta un preu inacceptable."""
    
        def __init__(self, titol, preu):
            super().__init__(titol)
            self.titol = titol
            self.preu = preu
    
        def __str__(self):
            return f"preu invalid per a '{self.titol}': {self.preu}"
    

    El menú la gestiona sense canvis perquè except ErrorPapyrus: atrapa la classe i totes les seves subclasses, incloses les que encara no existien quan es va escriure el menú. És el mateix polimorfisme del mòdul 5 — codi escrit contra la classe base que funciona amb qualsevol filla — aplicat al canal d'errors: la jerarquia d'excepcions és una interfície extensible.

Conclusió

Papyrus ja té el seu propi vocabulari d'errors: ErrorPapyrus com a arrel que agrupa, i penjant-ne LlibreNoTrobatError (amb titol), EstocInsuficientError (amb titol, demanat, disponible) i SociInvalidError (amb codi) — totes a casa seva, errors.py, amb atributs que el codi llegeix sense trossejar strings i un __str__ que compon el missatge des d'una única font de veritat. Has vist el dial de precisió (capturar per la fulla a la caixa, per la base al menú, i com el polimorfisme fa que les fulles futures es gestionin gratis) i la convivència amb criteri de cercar_llibre() tolerant i obtenir_llibre() estricta, calcant el duo get()/[] dels diccionaris. El sistema d'errors està complet... per al desenvolupament. Però quan la botiga funcioni de debò — l'Anna atenent, el programa corrent hores — els print() d'avís desapareixeran en tancar el terminal, i amb ells tota l'evidència. L'última lliçó del mòdul porta la peça de producció: el mòdul logging, amb nivells, format, fitxer papyrus.log i el valuosíssim logger.exception(); i tanca amb el decàleg de bones pràctiques que resumeix tot el mòdul 7.

Curs de Programació en Python

Mòdul 1: Introducció a Python

Mòdul 2: Estructures de Control

Mòdul 3: Funcions i Mòduls

Mòdul 4: Estructures de Dades

Mòdul 5: Programació Orientada a Objectes

Mòdul 6: Gestió de Fitxers

Mòdul 7: Gestió d'Errors i Excepcions

Mòdul 8: Temes Avançats

Mòdul 9: Proves i Depuració

Mòdul 10: Desenvolupament Web amb Python

Mòdul 11: Ciència de Dades amb Python

Mòdul 12: Projecte Final

© Copyright 2026. Tots els drets reservats