Fins ara les excepcions ens arribaven: les llançava float(), open(), json.load(). Però el mòdul 5 amagava un avançament que va passar gairebé desapercebut: el __post_init__ de Llibre i les properties de Producte ja feien raise ValueError(...) quan algú intentava un preu negatiu. Aleshores va ser un acte de fe; avui es converteix en decisió de disseny. En aquesta lliçó aprendràs a llançar excepcions amb intenció: quan una situació mereix un raise i quan n'hi ha prou amb retornar None, com escriure missatges que estalviïn una sessió de depuració, com rellançar el que no et toca gestionar i com encadenar amb raise ... from e per no perdre el rastre de l'error original. L'exemple central serà la funció més delicada de Papyrus: vendre(), la que toca l'estoc i la caixa.
Contingut
raise: llançar una excepció a propòsit- El missatge importa: errors accionables
- Llançar o retornar
None? El criteri del contracte - Guard clauses: validar a la porta
vendre(titol, unitats): l'exemple central- Rellançar (
raisea seques) i encadenar (raise ... from e) - Revisitant el mòdul 5: properties i
__post_init__amb vocabulari complet
raise: llançar una excepció a propòsit
La sentència raise crea i llança una excepció, exactament igual que ho faria Python pel seu compte:
def aplicar_descompte(preu, percentatge):
if not 0 <= percentatge <= 1:
raise ValueError(f"percentatge ha d'estar entre 0 i 1, no {percentatge}")
return preu * (1 - percentatge)Anatomia: raise + una instància d'excepció (es construeix com qualsevol objecte: classe i arguments, normalment el missatge). A partir d'aquí, el mecanisme és el de 07-01: l'execució s'interromp, l'excepció es propaga cap amunt, i o algú l'atrapa amb el try/except de 07-02 o el programa acaba amb el seu traceback.
I quin tipus llançar? Mentre no tinguem excepcions pròpies (07-04), es trien les integrades respectant-ne el significat, el mateix de la taula de 07-01:
| Situació | Tipus adequat |
|---|---|
El valor no val, encara que el tipus sigui correcte (percentatge=3) |
ValueError |
El tipus no és l'esperat (unitats="dos") |
TypeError |
| S'ha buscat una clau/element que no hi és | KeyError / LookupError |
| L'operació no és vàlida en l'estat actual de l'objecte | RuntimeError (o una de pròpia, a 07-04) |
Llançar un ValueError quan el problema és de tipus despista qui depuri. El tipus de l'excepció és part del missatge.
El missatge importa: errors accionables
Compara aquests dos llançaments davant del mateix problema:
raise ValueError("error a unitats") # ❌ quin error? quin valor ha arribat?
raise ValueError(f"unitats ha de ser un enter positiu, no {unitats!r}") # ✅Un bon missatge respon tres preguntes: què s'esperava, què ha arribat i — si no és obvi — què cal fer-hi. El !r a la f-string (equival a aplicar-hi repr()) mostra el valor amb les seves cometes i el seu tipus visible: no '3' delata que ha arribat un string "3" en comptes de l'enter 3, un matís que no 3 amagaria. Recorda 07-01: el missatge del raise és exactament el que apareixerà a l'última línia del traceback, la primera que llegirà qui depuri. Escriu-lo per a aquesta persona — que d'aquí a tres mesos seràs tu.
Llançar o retornar None? El criteri del contracte
Aquesta és la decisió de disseny central de la lliçó, i el preu_soci() de l'exercici 3 de 07-02 la va deixar oberta. La brúixola és la idea de contracte: tota funció promet alguna cosa ("dona'm un títol i unes unitats vàlides i descomptaré estoc"). Aleshores:
- Llança una excepció quan qui crida trenca el contracte — demana una cosa absurda o impossible: vendre −3 unitats, un preu negatiu, un codi de soci amb format invàlid. Això és un error de programació o d'ús i ha de fer soroll.
- Retorna
Nonequan l'absència és una resposta legítima a una pregunta ben formulada: "teniu Rayuela?" — no tenir-lo no és cap error, és informació.
| Criteri | Retornar None |
Llançar excepció |
|---|---|---|
| La situació és un resultat normal del negoci? | Sí: "no el tenim" és una resposta | No: és una violació del contracte |
| Qui en té la culpa? | Ningú: la pregunta era vàlida | Qui crida: ha demanat una cosa invàlida |
| Què farà qui crida, gairebé sempre? | Comprovar i continuar (if llibre is None:) |
No hauria de passar; si passa, millor soroll que silenci |
| Risc si s'ignora? | Baix: None explota aviat i a prop (AttributeError) |
Alt: continuar amb estoc negatiu corromp dades |
| Exemple Papyrus | cercar_llibre("Rayuela") → None |
vendre("Hamlet", -3) → ValueError |
El cas canònic de la columna esquerra és cercar_llibre(), que formalitzem així:
def cercar_llibre(cataleg, titol):
"""Retorna el Llibre corresponent al titol, o None si no hi es.
Que un llibre no sigui al cataleg NO es cap error: es la resposta
a la pregunta 'El teniu?'. Per aixo None, i no una excepcio.
"""
return cataleg.get(titol.strip().casefold())dict.get() (mòdul 4) retorna None quan la clau no existeix: encaixa com un guant amb aquest contracte. Qui crida ha de comprovar el resultat — if llibre is None: — i decidir. Compte, això sí, amb el None que viatja sense comprovar-se: acabarà en un AttributeError: 'NoneType' object has no attribute 'preu' dues funcions més enllà. Per això a 07-04 crearem una germana estricta, obtenir_llibre(), que llança en comptes de retornar None, i veurem que totes dues poden conviure: cadascuna serveix un tipus de cridador diferent.
Guard clauses: validar a la porta
Una guard clause (clàusula de guarda) és una comprovació al principi de la funció que llança immediatament si un argument no compleix el contracte. És LBYL del bo — el que 07-01 va declarar legítim: validar dades que ja tens a la mà, sense finestra temporal ni món exterior pel mig.
def registrar_soci(nom, codi):
# --- guardes: la porta d'entrada ---
if not nom.strip():
raise ValueError("el nom del soci no pot estar buit")
if not codi.strip():
raise ValueError("el codi del soci no pot estar buit")
# --- a partir d'aqui, terreny segur ---
...Avantatges de validar a la porta i no a mitja funció:
- Fallar aviat (fail fast): l'excepció salta a la primera línia, amb la dada acabada d'arribar, no vint línies després quan ja és difícil saber d'on ha vingut el valor dolent. Un estoc negatiu detectat en vendre és un misteri; detectat en entrar, és un traceback que assenyala el culpable.
- El cos respira: superades les guardes, la resta de la funció pot assumir dades vàlides sense
ifniats. - El contracte queda escrit: les guardes documenten, millor que cap comentari, què accepta la funció.
vendre(titol, unitats): l'exemple central
Ajuntem-ho tot a la funció més sensible de Papyrus. Vendre toca l'estoc i la caixa: aquí un error silenciós costa diners, així que el contracte es defensa amb raise:
IVA_LLIBRES = 0.04
def vendre(cataleg, titol, unitats):
"""Descompta estoc i retorna la quantia amb IVA de la venda.
Llanca ValueError si les unitats no son valides o si la venda
es impossible (titol desconegut o estoc insuficient).
"""
# Guardes sobre els arguments: qui crida trenca el contracte
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}")
# Guardes sobre l'estat: la venda demanada es impossible
llibre = cercar_llibre(cataleg, titol)
if llibre is None:
raise ValueError(f"'{titol}' no es al cataleg")
if llibre.estoc < unitats:
raise ValueError(
f"estoc insuficient de '{llibre.titol}': "
f"en queden {llibre.estoc}, se n'han demanat {unitats}"
)
# Terreny segur: executar la venda
llibre.estoc -= unitats
return round(llibre.preu * unitats * (1 + IVA_LLIBRES), 2)Decisions de disseny, una a una:
unitats <= 0ésValueError, no unreturn None: demanar −3 exemplars no és "una resposta legítima del negoci", és un cridador equivocat (o un bug aigües amunt). Soroll, no silenci.vendre()fa servircercar_llibre()per dins i tradueix el seuNonea excepció. És un patró elegant: la funció tolerant respon preguntes; la funció que executa accions converteix l'absència en violació de contracte, perquè vendre el que no existeix sí que és un error.- El missatge d'estoc porta els tres números que importen: quin llibre, quant en queda, quant se n'ha demanat. A 07-04 aquestes tres dades ascendiran a atributs d'una excepció pròpia (
EstocInsuficientError), perquè el codi — no només l'humà — les pugui llegir. - La mutació va al final. Totes les guardes s'avaluen abans de tocar
llibre.estoc. Si alguna cosa falla, el catàleg queda intacte: res de vendes a mitges.
Amb dades canòniques: vendre(cataleg, "Hamlet", 2) → els 6 d'estoc passen a 4 i retorna 9.95 * 2 * 1.04 = 20.70. vendre(cataleg, "Hamlet", 99) → ValueError: estoc insuficient de 'Hamlet': en queden 6, se n'han demanat 99 (res, ni el 6, no ha canviat).
Rellançar (raise a seques) i encadenar (raise ... from e)
De vegades atrapes una excepció però descobreixes que no et toca gestionar-la del tot. Dues eines:
raise a seques dins d'un except rellança la mateixa excepció, amb el seu traceback original intacte. Útil per reaccionar de passada (avisar, registrar, netejar) sense apropiar-te de l'error:
try:
quantia = vendre(cataleg, titol, unitats)
except ValueError:
print("Venda fallida; el cataleg NO s'ha modificat.")
raise # que decideixi qui hi ha mes amuntraise NovaExcepcio(...) from e llança una excepció diferent encadenada a l'original. És l'eina per traduir errors tècnics de baix nivell a l'idioma de qui crida, sense destruir l'evidència:
def carregar_cataleg():
ruta = BASE / "dades" / "cataleg.json"
try:
with open(ruta, encoding="utf-8") as f:
llista = json.load(f)
except json.JSONDecodeError as e:
raise RuntimeError(
f"cataleg no llegible a {ruta}; restaura una copia de copies/"
) from e
return {l["titol"].strip().casefold(): Llibre(**l) for l in llista}El traceback resultant mostra les dues excepcions, unides per la línia The above exception was the direct cause of the following exception: — a dalt el JSONDecodeError amb la línia exacta del JSON trencat, a sota el RuntimeError amb el consell de restaurar la còpia. Qui crida gestiona un error d'alt nivell; qui depura conserva la causa arrel. (Nota per comparar: a 07-02 aquest cas el vam resoldre amb SystemExit(1), decisió de programa-terminal; from e és la versió de biblioteca, que informa sense decidir per qui crida. A 07-04, RuntimeError cedirà el lloc a una excepció pròpia.)
I què passa si dins d'un except llances sense from? Python encadena igualment, però amb el rètol During handling of the above exception, another exception occurred: — que suggereix un accident dins del gestor, no una traducció deliberada. from e declara la intenció; fes-lo servir sempre que tradueixis.
| Eina | Què fa | Quan fer-la servir |
|---|---|---|
raise Tipus("msg") |
Llança una excepció nova | Violació de contracte detectada per tu (guardes) |
raise a seques (en except) |
Rellança l'actual, traceback intacte | Reaccionar de passada sense apropiar-te de l'error |
raise Tipus("msg") from e |
En llança una altra, encadenant la causa | Traduir un error tècnic a l'idioma de qui crida |
Revisitant el mòdul 5: properties i __post_init__ amb vocabulari complet
Tanquem el cercle. Això vam escriure al mòdul 5, aleshores com a recepta:
@dataclass
class Llibre:
titol: str
preu: float
estoc: int = 0
def __post_init__(self):
if self.preu < 0:
raise ValueError(f"el preu no pot ser negatiu: {self.preu}")
if self.estoc < 0:
raise ValueError(f"l'estoc no pot ser negatiu: {self.estoc}")Amb el vocabulari d'avui, cada peça té nom: __post_init__ és una guard clause del constructor — valida a la porta, falla aviat, i garanteix l'invariant que no pot existir un Llibre amb preu negatiu a tot Papyrus. Llança ValueError perquè el tipus és correcte i el valor no. El setter de la property preu de Producte fa el mateix a cada assignació posterior: el contracte no se signa només en néixer, es defensa tota la vida de l'objecte. I això explica una cosa de 07-02: quan carregar_cataleg() reconstrueix amb Llibre(**dades), un JSON amb "preu": -5 dispara aquest ValueError automàticament — la validació del mòdul 5 protegeix també la frontera de la persistència del mòdul 6, gratis. Els sistemes ben dissenyats encaixen així.
Errors Comuns i Consells
- Retornar codis d'error (
-1,False,"") en comptes de llançar. L'error viatja disfressat de dada i explota lluny, il·legible. En Python, contracte trencat = excepció; absència legítima =Nonedocumentat. - Llançar per a tot, fins i tot per a absències normals. L'extrem oposat: si
cercar_llibre()llancés per cada títol no trobat, consultar el catàleg s'ompliria detry/excepttrivials. Aplica la taula del contracte, no un reflex. - Atrapar la teva pròpia excepció dues línies després. Si llances i atrapes a la mateixa funció, gairebé segur que el
raisesobrava: era un simpleif.raiseés per creuar fronteres de responsabilitat. - Perdre la causa original en traduir.
except JSONDecodeError: raise RuntimeError("cataleg no llegible")sensefrom edeixa la traducció amb aspecte d'accident. Ambfrom ela cadena queda explícita i completa. - Missatges sense la dada culpable.
raise ValueError("unitats invalides")obliga a depurar;raise ValueError(f"unitats ha de ser positiu, no {unitats!r}")s'arregla d'un cop d'ull. - Consell: escriu primer la docstring amb el contracte ("llança X si..."), després les guardes que el fan complir. Si no saps què prometre, encara no saps què llançar.
- Consell: muta l'estat sempre després de totes les guardes. Una funció que valida a mitges i modifica a mitges deixa dades corruptes, que són pitjors que qualsevol excepció.
Exercicis
reposar(cataleg, titol, unitats). Escriu la funció que suma estoc a un llibre existent. Contracte:TypeErrorsiunitatsno ésint;ValueErrorsiunitats <= 0;ValueErrorsi el títol no és al catàleg (l'Anna no reposa el que no ha donat d'alta). Retorna l'estoc resultant. Comprova-la reposant 5 exemplars de Faust (que M6 va deixar en 5): ha de retornar 10 i els casos invàlids han de llançar amb missatges que incloguin la dada culpable.Noneoraise? Decideix i justifica en una frase, fent servir la taula del contracte: (a)cercar_soci(codi)quan"LLUIS-001"no és asocis.json; (b) la mateixa funció quan repcodi=""; (c)preu_amb_iva(preu)quanpreués negatiu; (d)darrera_venda()quanvendes.csvés buit després d'un dia sense vendes.- Traduir amb
from. Escriucarregar_socis()que llegeixidades/socis.jsoni, si el JSON és corrupte, llanciRuntimeError("fitxer de socis no llegible; restaura una copia")encadenat a l'error original ambfrom. Si el fitxer no existeix, ha de retornar[](per què aquesta asimetria entre les dues fallades?).
Solucions
-
def reposar(cataleg, titol, unitats): """Suma unitats a l'estoc d'un llibre ja catalogat. Llanca TypeError/ValueError si es viola el contracte. Retorna l'estoc resultant. """ 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 = cercar_llibre(cataleg, titol) if llibre is None: raise ValueError(f"'{titol}' no es al cataleg; dona'l d'alta primer") llibre.estoc += unitats return llibre.estocreposar(cataleg, "Faust", 5)→ 10. L'estructura calca la devendre(): guardes d'arguments, guarda d'estat, mutació al final. Quan dues funcions comparteixen contracte tan de prop, estan demanant una jerarquia d'excepcions comuna — 07-04. -
(a)
None: preguntar per un soci que no existeix és una consulta legítima; l'absència és informació ("no és soci → sense descompte"). (b)raise ValueError: un codi buit no és una pregunta vàlida sinó un cridador trencat — guard clause. La mateixa funció pot així retornarNonei llançar: responen a situacions diferents. (c)raise ValueError: un preu negatiu viola el contracte; retornarNonedeixaria passar el bug en silenci. (d)None(o[]si retorna una llista): un dia sense vendes és un resultat normal del negoci, no un error — que el tancament de caixa mostri 0.00 €, no un traceback. -
def carregar_socis(): ruta = BASE / "dades" / "socis.json" try: with open(ruta, encoding="utf-8") as f: return json.load(f) except FileNotFoundError: return [] except json.JSONDecodeError as e: raise RuntimeError("fitxer de socis no llegible; restaura una copia") from eL'asimetria és la de 07-02, ara amb nom de contracte: fitxer absent és un estat legítim (encara no hi ha socis: llista buida, com el
Nonede la taula); fitxer corrupte vol dir que hi ha dades però no les podem llegir — retornar[]les donaria per perdudes en silenci i el següent desat les aixafaria. Contracte trencat pel món exterior: excepció, traduïda a l'idioma de Papyrus amb la causa encadenada.
Conclusió
raise ha convertit Papyrus de reactiva en assertiva: les seves funcions ja no només encaixen errors aliens, sinó que defensen contractes propis. Tens el criteri (None per a absències legítimes com cercar_llibre(), excepció per a contractes trencats com vendre() amb unitats negatives), la tècnica (guard clauses que fallen aviat i muten tard, missatges amb la dada culpable en !r) i les eines de propagació (raise a seques per no apropiar-te de l'error, raise ... from e per traduir sense destruir l'evidència). Fins i tot les validacions del mòdul 5 han revelat el seu veritable nom: guardes de constructor que protegeixen invariants. Però fixa't en una esquerda: vendre() llança ValueError tant per "unitats negatives" com per "títol desconegut" com per "estoc insuficient" — i qui atrapi aquest ValueError no pot distingir-los sense llegir el missatge amb els ulls. El codi mereix alguna cosa millor que analitzar strings. La propera lliçó crea la jerarquia d'excepcions pròpia de Papyrus — ErrorPapyrus, LlibreNoTrobatError, EstocInsuficientError — amb atributs que les màquines puguin llegir, no només els humans.
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
