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
- Per què crear excepcions pròpies
- La forma mínima: heretar d'
Exception - La jerarquia de Papyrus:
ErrorPapyrusi les seves filles - Enriquir amb atributs i
__str__ - Capturar per la base vs capturar per la fulla
obtenir_llibre()vscercar_llibre(): estricta i tolerant conviuen- Integració a
vendre()i el mòdulerrors.py
Per què crear excepcions pròpies
Tres raons, de més pràctica a més profunda:
- L'
exceptde qui crida distingeix el TEU error de la resta.except EstocInsuficientError:atrapa exactament "no queda gènere" i deixa passar qualsevolValueErroraccidental (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.disponiblei 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.pydirà 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:
Amb això ja es pot llançar i atrapar:
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 = codiDesmuntem 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 enExceptionperquè el mecanisme estàndard (el text del traceback,print(e)) continuï funcionant. No l'ometis: una excepció que no cridasuper().__init__perd comportaments que altres esperen.- Els atributs (
self.titol,self.demanat,self.disponible,self.codi) són la càrrega útil: l'exceptque l'atrapi podrà calcular amb ells. __str__(el mateix dunder de M5) controla com es mostra l'excepció al traceback i en ferprint(e).EstocInsuficientErrorel 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 llibrecercar_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/ValueErrorcontinuen 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 aobtenir_llibre(), i l'excepció que aquesta llança travessavendre()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 algunexceptreal 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 existeixe.disponiblegarantit (podria ser unSociInvalidError). Si necessites els atributs d'una fulla, captura la fulla. - Consell: anomena amb la convenció
AlgunaCosaErrori escriu una docstring d'una línia a cada classe:errors.pyes 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
exceptde 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
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çarSociInvalidErrorsicodino és a la llistasocis(fes servir["LLUIS-001", "MARTA-002", "PAU-003"]), iLlibreNoTrobatErrorsi el títol no existeix (pista: no la llancis tu — deixa queobtenir_llibre()treballi). Comprova que("Hamlet", "MARTA-002")retorna9.45i que"JULIA-999"llança ambe.codi == "JULIA-999".- El taulell que no cau. Escriu un bucle
while Truede taulell que demani títol i unitats (amb eldemanar_enter()de 07-02), cridivendre()i: davant deLlibreNoTrobatErrorproposi encarregar-lo; davant d'EstocInsuficientErrorofereixi vendre lese.disponiblerestants si són > 0; davant de qualsevol altreErrorPapyrusmostri el missatge i continuï; i surti netament quan el títol sigui"fi". Per què l'ordre d'aquests tresexceptno pot ser cap altre? - Fulla nova sense tocar el menú. Afegeix a
errors.pyl'excepcióPreuInvalidError(ErrorPapyrus)amb atributstitolipreui 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
-
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"saltaSociInvalidError: codi de soci invalid: 'JULIA-999'ie.codival"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). -
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'elsemanté el missatge d'èxit fora deltry, com mana 07-02. -
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
- Introducció a Python
- Configuració de l'Entorn de Desenvolupament
- Sintaxi de Python i Tipus de Dades Bàsics
- Variables i Constants
- Entrada i Sortida Bàsica
- Entorns Virtuals i Gestió de Paquets
Mòdul 2: Estructures de Control
Mòdul 3: Funcions i Mòduls
- Definició de Funcions
- Arguments de Funció
- Funcions Lambda
- Mòduls i Paquets
- Visió General de la Biblioteca Estàndard
Mòdul 4: Estructures de Dades
Mòdul 5: Programació Orientada a Objectes
Mòdul 6: Gestió de Fitxers
- Lectura i Escriptura de Fitxers
- Treball amb Fitxers CSV
- Gestió de Dades JSON
- Operacions amb Fitxers i Directoris
Mòdul 7: Gestió d'Errors i Excepcions
- Introducció a les Excepcions
- Gestió d'Excepcions
- Llançament d'Excepcions
- Excepcions Personalitzades
- Bones Pràctiques i Registre d'Errors amb logging
Mòdul 8: Temes Avançats
- Anotacions de Tipus (type hints)
- Decoradors
- Generadors
- Gestors de Context
- Concurrència: Fils i Processos
- Asyncio per a la Programació Asíncrona
Mòdul 9: Proves i Depuració
- Introducció a les Proves
- Proves Unitàries amb unittest
- Proves amb pytest
- Desenvolupament Guiat per Proves
- Tècniques de Depuració
- Ús de pdb per a la Depuració
Mòdul 10: Desenvolupament Web amb Python
- Introducció al Desenvolupament Web
- Fonaments del Framework Flask
- Construcció d'APIs REST amb Flask
- Introducció a Django
- Construcció d'Aplicacions Web amb Django
Mòdul 11: Ciència de Dades amb Python
- Introducció a la Ciència de Dades
- NumPy per a la Computació Numèrica
- Pandas per a la Manipulació de Dades
- Matplotlib per a la Visualització de Dades
- Introducció a l'Aprenentatge Automàtic amb scikit-learn
