A la lliçó anterior vam aprendre a llegir l'avís; en aquesta aprenem a fer-nos-en càrrec. L'eina és l'estructura try/except, i amb ella Papyrus saldarà avui tres deutes concrets: carregar_cataleg() deixarà de morir quan cataleg.json no existeixi (adeu al pal·liatiu Path.exists()), el peatge str→float del CSV deixarà de "resar perquè porti números" i sobreviurà a files corruptes, i l'int(input()) que arrossega el seu ValueError des del mòdul 1 — apedaçat al mòdul 2 amb isdigit() — rebrà per fi la seva validació definitiva. Pel camí veurem les quatre peces de l'estructura completa (try, except, else, finally), com capturar l'objecte excepció amb as e, i una taula d'antipatrons encapçalada pel pitjor de tots: l'except: nu.
Contingut
- L'estructura
try/except: demanar perdó, per fi - Capturar tipus concrets (i per què mai un
except:nu) - Diversos
excepti capturar l'objecte ambas e - Les peces restants:
elseifinally - Papyrus, versió robusta:
carregar_cataleg()i el CSV de vendes - Saldar el deute del mòdul 1:
demanar_enter()
L'estructura try/except: demanar perdó, per fi
La sintaxi mínima té dos blocs: el que intentes i el que fas si falla:
try:
preu = float(text) # l'intent
except ValueError:
print(f"'{text}' no es un preu valid.") # el pla B
preu = 0.0Com flueix l'execució, pas a pas:
- Python executa el bloc
trylínia a línia. - Si no salta cap excepció, el bloc
excepts'ignora del tot i la vida continua. - Si una línia del
tryllança una excepció, l'execució abandona eltryimmediatament (les línies restants del bloc no s'executen) i Python busca unexceptel tipus del qual coincideixi. - Coincidir vol dir: l'excepció és d'aquella classe o d'una subclasse — la jerarquia de 07-01 en acció. Un
except LookupError:atrapa tantKeyErrorcomIndexError. - Si cap
exceptno coincideix, l'excepció continua propagant-se cap amunt, com si eltryno existís.
El punt 5 és important: try no és cap escut màgic. Només atrapa allò que li dius que atrapi. I això és una virtut, com veurem ara mateix.
Capturar tipus concrets (i per què mai un except: nu)
La temptació del principiant és "que no falli res": embolicar-ho tot i atrapar-ho tot. És l'error més car de tot el mòdul, perquè converteix errors sorollosos en errors silenciosos — i un error silenciós pot costar diners a l'Anna sense que ningú se n'assabenti.
# ❌ L'ANTIPATRO: no facis mai aixo
try:
cataleg = carregar_cataleg()
except: # atrapa TOT, fins i tot el que no hauria
cataleg = {} # ...i ho amaga sota la catifaQuè amaga aquest except: nu? Tot. Si hi ha una errada catalg dins de carregar_cataleg() (un NameError, un bug teu), el programa arrenca amb el catàleg buit i ningú no sabrà mai per què. Si l'Anna prem Ctrl+C per cancel·lar (KeyboardInterrupt, que hereta de BaseException i també cau en un except: nu), el programa ho ignora. El traceback — aquell informe forense gratuït de 07-01 — va directe a les escombraries.
| Antipatró | Per què és nociu | Alternativa correcta |
|---|---|---|
except: nu |
Atrapa fins i tot KeyboardInterrupt i SystemExit; oculta bugs propis |
except TipusConcret: |
except Exception: com a reflex |
Menys greu (deixa passar el Ctrl+C) però continua barrejant bugs amb fallades esperables | Capturar el tipus que saps gestionar; Exception només a la frontera del programa (07-05) |
except ValueError: pass |
Silencia l'error sense deixar-ne cap rastre | Com a mínim, avisar o comptar; idealment registrar-ho al log (07-05) |
try gegant al voltant de 30 línies |
Impossible saber quina línia ha fallat ni si l'except gestiona el que toca |
try mínim: només la línia (o línies) que poden fallar d'aquella manera |
Atrapar i retornar un valor "normal" (return -1) |
L'error es disfressa de dada i explota més lluny, on és il·legible | Retornar None amb criteri (07-03) o deixar propagar |
La regla que resumeix la taula: captura només allò que esperes i saps gestionar; deixa que tota la resta exploti. Una excepció no gestionada amb el seu traceback és infinitament millor que un programa que menteix.
Diversos except i capturar l'objecte amb as e
Un mateix try pot fallar de maneres diferents que mereixen reaccions diferents. S'encadenen diversos except, i Python fa servir el primer que coincideixi (per això els tipus específics van abans que els genèrics):
try:
with open(ruta, encoding="utf-8") as f:
dades = json.load(f)
except FileNotFoundError:
print("No hi ha cataleg previ. Es partira d'un de buit.")
dades = []
except json.JSONDecodeError as e:
print(f"El cataleg esta corrupte ({e}). Revisa el fitxer o restaura una copia.")
raise SystemExit(1)Dues novetats aquí:
as eguarda l'objecte excepció a la variablee, disponible dins del bloc. Recorda de 07-01 que l'excepció és un objecte:print(e)en mostra el missatge, i alguns tipus porten atributs extres (e.linenoaJSONDecodeErrordiu a quina línia del JSON hi ha la destrossa). La variable només existeix dins del seu blocexcept.json.JSONDecodeErrorés la resposta a una altra pregunta del tancament de M6: "i si la còpia és corrupta?". Si algú va editar malamentcataleg.jsona mà — el mòdul 6 només va poder suplicar "no editis malament el JSON" —,json.load()llança aquesta excepció, i ara sabem distingir fitxer absent (recuperable: catàleg buit) de fitxer corrupte (no t'inventis dades: avisa i atura't).
Si diverses excepcions mereixen la mateixa reacció, s'agrupen en una tupla: except (ValueError, KeyError) as e:.
Les peces restants: else i finally
L'estructura completa té quatre blocs, cadascun amb un paper precís:
try:
f = open(ruta, encoding="utf-8") # pot fallar
except FileNotFoundError:
print("Fitxer no trobat.") # pla B
else:
contingut = f.read() # nomes si el try HA ANAT BE
f.close()
finally:
print("Fi de l'intent de lectura.") # SEMPRE, passi el que passi| Bloc | S'executa quan... | Per a què serveix |
|---|---|---|
try |
Sempre (és l'intent) | La(les) operació(ns) que poden fallar |
except |
Només si el try ha llançat una excepció coincident |
El pla B: recuperar-se, avisar, registrar |
else |
Només si el try ha acabat sense excepció |
La feina que depèn de l'èxit, fora del try per no atrapar-ne els errors per accident |
finally |
Sempre: amb èxit, amb excepció gestionada, amb excepció no gestionada i fins i tot amb un return pel mig |
Neteja garantida: tancar, alliberar, deixar-ho tot en ordre |
Per què molestar-se amb else en comptes d'escriure-ho tot dins del try? Precisió: si f.read() fos dins del try i fallés per un altre motiu, un except massa ample podria atrapar-lo i diagnosticar malament. El try ha d'abrigar només les línies la fallada de les quals saps gestionar; else recull la continuació del camí feliç.
I finally? Et sonarà la garantia: és exactament el que el with del mòdul 6 fa per tu amb els fitxers — tancar passi el que passi. De fet, with està construït sobre aquesta mecànica, i al mòdul 8 aprendrem a fabricar els nostres propis context managers. Mentrestant, la parella pràctica és: with per a fitxers, finally per a qualsevol altra neteja que hagi de passar sí o sí (imprimir un resum de tancament, restaurar un estat).
flowchart TD
A[try] -->|sense excepció| B[else]
A -->|excepció coincident| C[except]
A -->|excepció NO coincident| D[es propaga cap amunt]
B --> E[finally]
C --> E
D --> E
E -->|si venia propagant-se| F[continua propagant-se]
Papyrus, versió robusta: carregar_cataleg() i el CSV de vendes
És hora de pagar els deutes del mòdul 6. Així va quedar carregar_cataleg() aleshores, pal·liatiu inclòs:
# Versio M6 — amb pal.liatiu (LBYL)
def carregar_cataleg():
ruta = BASE / "dades" / "cataleg.json"
if not ruta.exists(): # LBYL: foto del passat
return {}
with open(ruta, encoding="utf-8") as f:
llista = json.load(f)
return {l["titol"].strip().casefold(): Llibre(**l) for l in llista}I així queda la versió EAFP, que a més distingeix absent de corrupte:
# Versio M7 — EAFP
def carregar_cataleg():
ruta = BASE / "dades" / "cataleg.json"
try:
with open(ruta, encoding="utf-8") as f:
llista = json.load(f)
except FileNotFoundError:
print("Avis: no existeix cataleg.json. Es parteix d'un cataleg buit.")
return {}
except json.JSONDecodeError as e:
print(f"Error: cataleg.json esta corrupte ({e}).")
print("Restaura una copia de copies/ abans de continuar.")
raise SystemExit(1)
return {l["titol"].strip().casefold(): Llibre(**l) for l in llista}Punt per punt:
- El
trycobreix només l'obertura i el parseig — les dues operacions que poden fallar d'aquestes maneres. La construcció del diccionari queda fora: siLlibre(**l)fallés (un preu negatiu al JSON dispararia elValueErrordel__post_init__de M5), volem veure aquell traceback, no confondre'l amb un problema de fitxer. FileNotFoundErrorés un cas esperat i recuperable: primera arrencada en un ordinador nou. Catàleg buit + avís honest. Sense condició de carrera: l'open()és la comprovació.JSONDecodeErrorés esperat però no recuperable en silenci: inventar-se un catàleg buit quan n'existeix un de corrupte faria que l'Anna aixafés les seves dades reals en desar. Avisem amb el detall de l'error i parem. Gestionar bé una excepció de vegades vol dir aturar-se millor, no continuar a qualsevol preu.
Segon deute: el peatge str→float de vendes.csv (06-02). Si una fila porta "gratis" com a quantia, la versió M6 mor en plena suma. La M7 salta la fila corrupta, compta els errors i continua:
def tancament_de_caixa():
ruta = BASE / "dades" / "vendes.csv"
total, vendes_ok, files_corruptes = 0.0, 0, 0
try:
with open(ruta, encoding="utf-8", newline="") as f:
for fila in csv.DictReader(f):
try:
total += float(fila["quantia"]) # el peatge str→float
except ValueError:
files_corruptes += 1
print(f" Fila ignorada, quantia no llegible: {fila!r}")
else:
vendes_ok += 1
except FileNotFoundError:
print("Avui no hi ha vendes registrades.")
return 0.0
if files_corruptes:
print(f"Atencio: {files_corruptes} fila/es corrupta/es a vendes.csv.")
print(f"Tancament de caixa: {vendes_ok} vendes, total {total:.2f} €")
return totalObserva el try niat i mínim: l'interior abriga una sola línia (la conversió) i atrapa un sol tipus (ValueError). Una fila dolenta ja no tomba el tancament del dia — però tampoc no desapareix: es compta i es mostra, perquè silenciar-la seria l'antipatró pass de la taula. L'else interior suma la venda al comptador només quan la conversió ha anat bé: exactament el paper que li hem assignat a else.
Saldar el deute del mòdul 1: demanar_enter()
El deute més antic del curs. Al mòdul 1, int(input()) explotava si la Júlia teclejava "tres". Al mòdul 2 ho vam apedaçar amb while True + isdigit(), i ja aleshores vam avisar que era provisional. Per què? Perquè isdigit() intenta predir el que int() acceptarà, i prediu malament: "-2".isdigit() és False (rebutja negatius legítims), " 4 ".isdigit() és False (rebutja espais que int() tolera). És LBYL amb un validador que no coincideix amb el conversor. La solució EAFP: que decideixi el mateix int().
def demanar_enter(missatge):
"""Demana un enter per teclat fins que l'entrada sigui valida."""
while True:
text = input(missatge)
try:
return int(text) # si converteix, retornem i sortim
except ValueError:
print(f"'{text}' no es un nombre enter. Torna-ho a provar.")
unitats = demanar_enter("Quants exemplars de Hamlet? ")- El
returndins deltrytrenca el bucle tan bon punt hi ha un enter vàlid — no calbreak. - Si
int()llançaValueError, l'exceptinforma (mostrant el que s'ha teclejat, missatge accionable) i elwhile Truetorna a intentar-ho. - El jutge és el mateix
int(): accepta"-2"i" 4 ", rebutja"tres"i"4.5". Zero desacords entre validador i conversor, perquè són la mateixa cosa.
La mateixa plantilla serveix per a demanar_preu() amb float(): guarda-la, perquè és una de les funcions més reutilitzades que escriuràs.
Errors Comuns i Consells
except:nu. L'antipatró número u. Atrapa fins i tot el Ctrl+C de l'Anna i enterra els teus propis bugs. Sempre un tipus concret.tryquilomètric. Si el bloc té deu línies, ja no saps quina ha fallat ni si el teuexceptés la reacció adequada. Encongeix eltryfins a la línia culpable; fes servirelseper a la continuació.- Silenciar amb
pass.except ValueError: passfa desaparèixer dades sense deixar rastre. Atancament_de_caixa()les files corruptes es compten i es mostren; a 07-05 aniran al log. - Ordenar malament els
except. Python fa servir el primer que coincideix: si posesexcept Exception:abans queexcept ValueError:, el segon és codi mort. De l'específic al general. - Oblidar que
elseexisteix. No és obligatori, però ficar el camí feliç dins deltry"perquè hi cap" acaba atrapant excepcions que no volies atrapar. - Consell: abans d'escriure un
except, pregunta't: "què faré amb aquest error?". Si la resposta és "res", probablement no l'hauries d'atrapar: deixa que es propagui cap a qui sí que hi pugui fer alguna cosa. - Consell: fixa't que els nostres missatges d'avís sempre inclouen la dada culpable (
'{text}',{fila!r}). Un avís sense la dada obliga a endevinar; amb la dada, s'arregla en un minut.
Exercicis
-
demanar_preu(). Escriu la bessona dedemanar_enter(): demana un preu ambfloat(), torna a intentar-ho davant deValueErrori, a més, rebutja preus negatius amb un missatge diferent ("un preu no pot ser negatiu") sense sortir del bucle. Prova-la amb"9,95"(coma decimal, molt habitual aquí): què passa i per què? -
carregar_config()a prova de tot. El mòdul 6 va deixarconfig.jsonamb{"nom_botiga", "iva_llibres", "descompte_soci"}. Escriucarregar_config()que: retorni el diccionari si tot va bé; davant deFileNotFoundErrorretorni la configuració per defecte{"nom_botiga": "Papyrus", "iva_llibres": 0.04, "descompte_soci": 0.05}amb un avís; i davant dejson.JSONDecodeErrormostri l'error i rellanciSystemExit(1). Afegeix-hi unfinallyque imprimeixi sempre"Configuracio: proces acabat."i comprova que apareix en els tres escenaris. -
Caçar l'antipatró. Aquest codi "funciona", però comet com a mínim tres pecats de la taula d'antipatrons. Identifica'ls i reescriu-lo:
def preu_soci(cataleg, titol): try: llibre = cataleg[titol.strip().casefold()] preu = llibre.preu * (1 - DESCOMPTE_SOCI) print("El preu per a socis de " + titol + " es " + str(round(preu, 2))) return preu except: return -1
Solucions
-
def demanar_preu(missatge): while True: text = input(missatge) try: preu = float(text) except ValueError: print(f"'{text}' no es un numero. Fes servir punt decimal, p. ex. 9.95") continue if preu < 0: print("Un preu no pot ser negatiu. Torna-ho a provar.") continue return preuAmb
"9,95"saltaValueError:float()només entén el punt decimal. Per això el missatge de l'exceptsuggereix el format correcte — missatge accionable. (Alternativa amable:text.replace(",", ".")abans de convertir.) Fixa't en l'ordre: primer la conversió (EAFP), després la validació del valor ja convertit (LBYL sobre una dada que ja tens a la mà, legítim segons vam veure a 07-01). -
CONFIG_DEFECTE = {"nom_botiga": "Papyrus", "iva_llibres": 0.04, "descompte_soci": 0.05} def carregar_config(): ruta = BASE / "dades" / "config.json" try: with open(ruta, encoding="utf-8") as f: return json.load(f) except FileNotFoundError: print("Avis: sense config.json; es fan servir els valors per defecte.") return dict(CONFIG_DEFECTE) except json.JSONDecodeError as e: print(f"config.json corrupte: {e}") raise SystemExit(1) finally: print("Configuracio: proces acabat.")El
finallys'imprimeix en els tres escenaris — fins i tot amb elsreturnpel mig i fins i tot quanSystemExits'està propagant. Aquesta és la seva garantia. Detall fi: es retornadict(CONFIG_DEFECTE)(una còpia) perquè ningú no muti el diccionari per defecte compartit, record de l'aliasing del mòdul 4. -
Pecats: (1)
except:nu — atraparia fins i tot unNameErrorsiDESCOMPTE_SOCIno estigués definit, ocultant un bug; (2)trymassa ample — només l'accéscataleg[...]pot fallar de manera esperable (KeyError); elprinti l'aritmètica no haurien d'estar abrigats; (3) retornar-1com a error disfressat de preu — qui rebi aquest-1podria cobrar-lo a la Júlia. Reescriptura:def preu_soci(cataleg, titol): try: llibre = cataleg[titol.strip().casefold()] except KeyError: print(f"'{titol}' no es al cataleg.") return None return round(llibre.preu * (1 - DESCOMPTE_SOCI), 2)I és
Noneel millor senyal de "no trobat"? O aquesta funció hauria de llançar la seva pròpia excepció? Aquesta pregunta exacta — quan retornarNonei quan ferraise— obre la propera lliçó.
Conclusió
Ja saps demanar perdó amb estil: try mínim al voltant de la línia que pot fallar, except de tipus concrets (mai nu), diversos except ordenats de l'específic al general, as e per aprofitar el missatge de l'objecte excepció, else per al camí feliç que depèn de l'èxit i finally per a la neteja incondicional. Papyrus ha pagat tres deutes: carregar_cataleg() distingeix "no hi ha catàleg" (recuperable) de "catàleg corrupte" (millor parar), tancament_de_caixa() sobreviu a files il·legibles comptant-les en comptes de morir o silenciar-les, i demanar_enter() jubila l'impostor isdigit() deixant que int() sigui el seu propi jutge. Fins ara només hem reaccionat a excepcions que Python llança pel seu compte. La propera lliçó inverteix els papers: aprendràs a llançar-les tu amb raise — decidir quan una situació mereix excepció i quan un simple None, rellançar, encadenar amb from, i protegir les teves funcions amb guard clauses. El raise ValueError que el __post_init__ de Llibre fa des del mòdul 5 deixarà de ser un acte de fe per convertir-se en una decisió de disseny.
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
