El mòdul 6 va acabar amb una llista incòmoda de "pal·liatius": Path.exists() abans de llegir, "no editis malament el JSON", "resa perquè el CSV porti números". Papyrus ja té memòria — catàleg, vendes, socis i còpies de seguretat viuen a dades/ — però aquesta memòria és fràgil: si cataleg.json no és on hauria de ser, si una fila del CSV porta "gratis" a la columna del preu, o si la Júlia tecleja un número impossible, el programa mor amb un bloc de text vermell a mitja pantalla. Aquest bloc de text té nom (traceback), aquest "morir" té mecànica (una excepció que ningú no atrapa), i tot aquest mòdul tracta de dominar totes dues coses. En aquesta primera lliçó encara no arreglarem res: entendrem què és exactament una excepció, aprendrem a llegir un traceback com qui llegeix un informe d'incidències, coneixerem les excepcions integrades que Papyrus ja ha patit sense saber-ne el nom, i assumirem la filosofia que Python prefereix: demanar perdó en comptes de demanar permís.
Contingut
- Què és una excepció: el mecanisme, no el desastre
- Anatomia d'un traceback: llegir-lo de baix a dalt
- Errors de sintaxi vs excepcions en temps d'execució
- Les excepcions integrades que Papyrus ja coneix (sense saber-ho)
- La jerarquia d'excepcions:
Exceptioncom a mare - EAFP vs LBYL: per què
Path.exists()era un pal·liatiu
Què és una excepció: el mecanisme, no el desastre
Una excepció és un objecte que Python crea quan una operació no es pot completar, i que interromp el flux normal del programa per viatjar cap amunt buscant algú que se'n faci càrrec. Tres idees clau:
- És un objecte. Igual que
Llibre("Hamlet", 9.95, 6)és una instància de la classeLlibre, unValueError("el preu no pot ser negatiu")és una instància de la classeValueError. Porta a dins un missatge i, com veurem a 07-04, pot portar atributs propis. - Es propaga. Quan passa, l'execució abandona la línia actual, surt de la funció, surt de la funció que l'havia cridat, i així successivament. Aquest viatge ascendent es diu propagació i és el que fa que un error a la línia 3 d'una funció auxiliar acabi apareixent com a fallada de tot el programa.
- Si ningú no l'atrapa, el programa acaba. Python imprimeix el traceback i retorna el control al sistema operatiu. Això és el que l'Anna ha estat veient fins ara: excepcions no gestionades.
Fixa't en el matís: l'excepció no és el problema. L'excepció és l'avís estructurat que hi ha hagut un problema. El desastre és no tenir ningú escoltant l'avís.
# fitxa_llibre.py — el mateix codi innocent del modul 1
preu_text = input("Preu del llibre: ") # Julia tecleja: gratis
preu = float(preu_text) # aqui neix l'excepcio
print(f"Amb IVA: {preu * 1.04:.2f} €") # aquesta linia no s'executa maiQuan float("gratis") no pot produir cap número, Python construeix un objecte ValueError, abandona la línia 3 i, com que ningú no l'atrapa, el programa acaba. La línia del print no arriba a executar-se mai: la propagació talla el flux en sec.
Anatomia d'un traceback: llegir-lo de baix a dalt
Això és el que imprimeix Python quan mor l'exemple anterior:
Traceback (most recent call last):
File "fitxa_llibre.py", line 2, in <module>
preu = float(preu_text)
ValueError: could not convert string to float: 'gratis'I aquest és un cas real amb diverses funcions encadenades, a l'estil de caixa.py del mòdul 6:
Traceback (most recent call last):
File "caixa.py", line 42, in <module>
cataleg = carregar_cataleg()
File "caixa.py", line 18, in carregar_cataleg
with open(BASE / "dades" / "cataleg.json", encoding="utf-8") as f:
FileNotFoundError: [Errno 2] No such file or directory: 'dades/cataleg.json'La regla d'or per llegir-lo: comença per baix.
- Última línia: el tipus d'excepció (
FileNotFoundError) i el seu missatge. És el què. Llegeix-la sempre primer. - Penúltima entrada (fitxer + línia + codi): el lloc exacte on va néixer l'excepció. És el on.
- D'allà cap amunt: la cadena de crides que hi va portar (
<module>va cridarcarregar_cataleg, que va executar l'open). És el com s'hi va arribar.most recent call lastvol dir literalment això: la crida més recent és al final.
| Part del traceback | Pregunta que respon | A l'exemple |
|---|---|---|
Última línia (Tipus: missatge) |
Què ha fallat? | No existeix dades/cataleg.json |
Última entrada File ... line ... |
On ha nascut? | Línia 18, dins de carregar_cataleg |
| Entrades superiors | Qui ha cridat a qui? | El programa principal (línia 42) ha cridat carregar_cataleg |
Un traceback no és cap càstig: és el millor informe d'incidències gratuït que existeix. Aprendre a llegir-lo amb calma t'estalviarà hores; copiar-lo sencer (última línia inclosa) és també la manera correcta de demanar ajuda.
Errors de sintaxi vs excepcions en temps d'execució
No tot el text vermell és igual. Hi ha dues famílies radicalment diferents:
Error de sintaxi (SyntaxError) |
Excepció en execució | |
|---|---|---|
| Quan apareix | Abans d'executar res: en compilar el fitxer | En plena execució, en arribar a la línia problemàtica |
| Depèn de les dades | No: falla sempre, amb qualsevol entrada | Sí: float(text) funciona amb "12.50" i explota amb "gratis" |
| Es pot gestionar amb el mecanisme d'aquest mòdul? | No: es corregeix el codi i punt | Sí: és exactament per a això que serveixen try/except (07-02) |
| Exemple Papyrus | Oblidar els dos punts d'un if |
El CSV de vendes porta una fila corrupta |
La conseqüència pràctica és important: un SyntaxError és sempre un bug teu i s'arregla editant; una excepció en execució pot ser un bug o pot ser el món real comportant-se com el món real (fitxers que no hi són, usuaris que teclegen qualsevol cosa). Aquest mòdul tracta la segona família.
Les excepcions integrades que Papyrus ja coneix (sense saber-ho)
Python porta desenes d'excepcions predefinides. Aquestes set cobreixen el 90 % del dia a dia, i totes tenen ja un episodi a la història de Papyrus:
| Excepció | Quan salta | Exemple Papyrus |
|---|---|---|
ValueError |
El tipus és correcte però el valor no val | float("gratis") en llegir vendes.csv; el __post_init__ de Llibre amb preu=-5 (M5) |
TypeError |
El tipus de dada no és l'esperat per a l'operació | "12.50" * 0.04 no falla (repeteix text), però "12.50" + 0.04 sí: no se suma str amb float |
KeyError |
Clau inexistent en un diccionari | cataleg["faust "] — l'espai final trenca la clau normalitzada amb strip().casefold() (M4) |
IndexError |
Índex fora de rang en una llista o tupla | darreres_vendes[10] quan avui només hi ha hagut 3 vendes |
FileNotFoundError |
S'intenta obrir un fitxer que no existeix | open(BASE / "dades" / "cataleg.json") al portàtil nou de l'Anna, on dades/ encara no existeix (M6) |
ZeroDivisionError |
Divisió entre zero | total_vendes / nombre_vendes a tancament_de_caixa() un dia sense vendes |
AttributeError |
L'objecte no té aquest atribut o mètode | llibre.titul (errada) en comptes de llibre.titol; o cridar .append() sobre el dict del catàleg |
Val la pena provocar-les a propòsit a l'intèrpret interactiu: tecleja int("Hamlet"), {}["faust"], [][0]... Veure el nom i el missatge de cadascuna en un entorn controlat fa que les reconeguis a l'instant quan apareguin en un traceback real.
>>> cataleg = {"hamlet": "Llibre(Hamlet, 9.95, 6)"}
>>> cataleg["Hamlet "] # hem oblidat normalitzar la clau
Traceback (most recent call last):
...
KeyError: 'Hamlet 'El missatge del KeyError mostra la clau buscada amb les seves cometes: allà es veu l'espai traïdor. Un altre motiu per llegir l'última línia amb lupa.
La jerarquia d'excepcions: Exception com a mare
Les excepcions són classes, i com tota classe de Python (ho vam veure al mòdul 5) poden heretar. Gairebé totes les que t'interessen descendeixen d'Exception:
graph TD
BE[BaseException] --> KI[KeyboardInterrupt<br/>Ctrl+C]
BE --> EX[Exception]
EX --> AE[ArithmeticError] --> ZD[ZeroDivisionError]
EX --> LE[LookupError] --> KE[KeyError]
LE --> IE[IndexError]
EX --> VE[ValueError]
EX --> TE[TypeError]
EX --> ATE[AttributeError]
EX --> OS[OSError] --> FNF[FileNotFoundError]
Idees que convé retenir ja (en traurem partit a 07-02 i 07-04):
Exceptionés la mare dels errors "normals": els que un programa pot gestionar de manera raonable.BaseExceptionés per sobre i inclou coses que gairebé mai no has d'atrapar, comKeyboardInterrupt(el Ctrl+C amb què l'Anna talla el programa a propòsit). Per això "atrapar-ho tot" és mala idea: podries impedir fins i tot que l'usuari cancel·li.- Les categories intermèdies agrupen:
KeyErroriIndexErrorsón totes duesLookupError("he buscat una cosa i no hi era");FileNotFoundErrorés unOSError(problema amb el sistema operatiu). Gestionar la categoria gestiona totes les seves filles — el mateix polimorfisme del mòdul 5, aplicat a errors. - A 07-04 estendrem aquest arbre amb excepcions pròpies de Papyrus, penjant d'
Exceptionexactament igual queValueError.
EAFP vs LBYL: per què Path.exists() era un pal·liatiu
El mòdul 6 en va dir pal·liatiu una vegada i una altra. Ara ho podem explicar amb vocabulari tècnic. Hi ha dues filosofies per tractar amb operacions que poden fallar:
- LBYL — Look Before You Leap ("mira abans de saltar"): comprovar les precondicions abans d'actuar.
if ruta.exists(): open(ruta). És el que vam fer al mòdul 6. - EAFP — Easier to Ask Forgiveness than Permission ("és més fàcil demanar perdó que permís"): actuar directament i gestionar l'excepció si alguna cosa surt malament. És l'estil idiomàtic de Python.
| Criteri | LBYL (if ruta.exists():) |
EAFP (intentar i gestionar) |
|---|---|---|
| Condició de carrera | Entre l'exists() i l'open() passa temps: un altre procés (la còpia nocturna!) pot esborrar o moure el fitxer entremig. La comprovació no garanteix res |
L'open() és la mateixa comprovació: o funciona o avisa, sense forat intermedi |
| Doble feina | El sistema busca el fitxer dues vegades: una per a exists(), una altra per a open() |
Una sola operació |
| Cobertura | exists() només cobreix "no existeix". I si existeix però és corrupte, o sense permisos de lectura, o és un directori? Necessitaries un if per cada cas |
L'excepció cobreix qualsevol fallada, fins i tot les que no havies anticipat |
| Llegibilitat | El camí feliç queda enterrat entre comprovacions | El camí feliç es llegeix net; la gestió d'errors va a part |
La condició de carrera (race condition) mereix un exemple concret: la còpia nocturna de Papyrus (copia_de_seguretat(), M6) reanomena i mou fitxers a dades/. Si caixa.py comprova cataleg_json.exists() → True, i una dècima de segon després el procés de còpia mou aquell fitxer, l'open() posterior explota malgrat la comprovació. LBYL dona una falsa sensació de seguretat: has verificat una foto del passat, no el present.
Compte: EAFP no vol dir "no validis mai". Validar dades d'entrada (és negatiu aquest preu que ha teclejat la Júlia?) continua sent correcte i ho refinarem a 07-03. El que EAFP desaconsella és intentar predir l'estat del món exterior (fitxers, xarxa, altres processos) amb comprovacions prèvies, quan el mateix intent ja et diu la veritat.
El que encara no sabem fer és "demanar perdó": gestionar l'excepció quan arriba. Aquest és exactament el tema de la propera lliçó.
Errors Comuns i Consells
- Llegir el traceback de dalt a baix i espantar-se a la segona línia. Al revés: última línia primer (tipus i missatge), després el punt d'origen, després la cadena de crides. De baix a dalt, sempre.
- Confondre
SyntaxErroramb una excepció gestionable. Si l'error apareix abans que s'executi res, és sintaxi: es corregeix el codi, no es "gestiona". - Pensar que
KeyError: 'Hamlet 'iKeyError: 'hamlet'són el mateix error. El missatge mostra la clau exacta entre cometes: espais i majúscules inclosos. A Papyrus, gairebé sempre vol dir que algú s'ha saltat la normalitzacióstrip().casefold(). - Creure que
Path.exists()"arregla" el problema del fitxer absent. Només cobreix una fallada entre moltes i deixa una finestra temporal oberta. És un pal·liatiu — el mòdul 6 ho va dir, i ara saps per què. - Consell: quan un traceback et desconcerti, reprodueix l'excepció a mà a l'intèrpret amb dades mínimes (
float("gratis"),{}["x"]). Aïllar la fallada del programa gran és la meitat del diagnòstic. - Consell: guarda els tracebacks que et trobis aquests dies. A 07-05 aprendràs a enviar-los automàticament a un fitxer de log en comptes de perdre'ls en tancar el terminal.
Exercicis
-
Detectiu de tracebacks. Sense executar res, prediu quina excepció (tipus exacte) llança cada fragment i per què: (a)
int("12.50"); (b)cataleg = {}seguit decataleg["hamlet"].preu; (c)vendes = []seguit devendes[0]; (d)"Faust" + 21.00. Després comprova-ho a l'intèrpret i compara el missatge real amb la teva predicció. -
Autòpsia d'un traceback. Donat aquest traceback, respon: què ha fallat?, a quina línia i funció ha nascut?, quina ha estat la cadena de crides completa?, quina dada ha provocat la fallada?
Traceback (most recent call last): File "caixa.py", line 31, in <module> total = tancament_de_caixa() File "caixa.py", line 22, in tancament_de_caixa quanties.append(float(fila["quantia"])) ValueError: could not convert string to float: 'dotze amb cinquanta' -
Classificar filosofies. Per a cada situació de Papyrus, decideix si hi encaixa millor LBYL o EAFP i justifica-ho en una frase: (a) obrir
dades/config.jsonen arrencar; (b) comprovar queunitatsés més gran que 0 abans de vendre; (c) accedir acataleg[clau]amb una clau teclejada per la Júlia; (d) comprovar quedades/existeix abans de crear-la ambmkdir.
Solucions
- (a)
ValueError: el tipus (str) és acceptable per aint(), però el valor"12.50"no representa un enter —int()no trunca cadenes decimals. (b)KeyError: 'hamlet': el diccionari és buit; fixa't que la fallada salta a l'accés[...], abans d'arribar tan sols a.preu. (c)IndexError: llista buida, no hi ha posició 0. (d)TypeError: no es pot concatenarstrambfloat; el missatge real diucan only concatenate str (not "float") to str. - Què: un
ValueErrorperquèfloat()ha rebut'dotze amb cinquanta', que no és convertible a número. On: línia 22, dins detancament_de_caixa. Cadena: el programa principal (<module>, línia 31) ha cridattancament_de_caixa(), que a la seva línia 22 ha intentat convertir el campquantiad'una fila del CSV. Dada culpable: algú va escriure la quantia amb lletres avendes.csv— exactament l'escenari "resa perquè el CSV porti números" del mòdul 6. - (a) EAFP: és estat del món exterior; intentar obrir i gestionar
FileNotFoundErrorevita la condició de carrera i cobreix més fallades queexists(). (b) LBYL (validació d'entrada): és una dada que ja tens a la mà; comprovar-la abans és barat, sense finestra temporal — a 07-03 veurem que la reacció correcta a aquesta comprovació ésraise. (c) EAFP:tryamb accés directe (o.get(), que vam veure a M4) és més pythònic queif clau in catalegseguit de l'accés — dues cerques en comptes d'una. (d) Truc: ni una cosa ni l'altra —ruta.mkdir(exist_ok=True)(M6) elimina el problema d'arrel; la millor comprovació és la que no cal fer.
Conclusió
Ja tens el mapa del territori: una excepció és un objecte que interromp el flux i es propaga cap amunt fins que algú l'atrapa o el programa mor; el traceback és el seu informe forense i es llegeix de baix a dalt; els errors de sintaxi són una altra espècie (es corregeixen, no es gestionen); les excepcions integrades formen una jerarquia de classes amb Exception com a mare, i Python prefereix EAFP — intentar i demanar perdó — davant del LBYL de Path.exists(), que deixa condicions de carrera obertes i només cobreix les fallades que vas saber anticipar. Però de moment continuem igual que l'Anna: veiem venir el cop i no sabem encaixar-lo. La propera lliçó introdueix l'eina central de tot el mòdul, try/except, i amb ella saldarem dos deutes històrics: carregar_cataleg() deixarà de dependre que cataleg.json existeixi, i l'int(input()) que arrossega el seu ValueError des del mòdul 1 tindrà per fi una validació digna.
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
