El mòdul ha anat deixant avisos pel camí: print("Avis: no existeix cataleg.json..."), print("Fila ignorada..."). Funcionaven a classe, però imagina l'escena real: l'Anna tanca la botiga un dissabte, apaga l'ordinador i el dilluns el tancament de caixa no quadra. Què va passar dissabte? Els print() van morir amb el terminal; no en queda ni rastre. Els avisos per pantalla serveixen per a qui està mirant; per diagnosticar el que va passar quan ningú no mirava cal un registre persistent: el mòdul logging de la biblioteca estàndard. En aquesta última lliçó del mòdul aprendràs els seus nivells, la seva configuració bàsica cap a dades/papyrus.log, el patró del logger per mòdul i la joia logger.exception(), que desa el traceback complet al fitxer. Tancarem amb el decàleg de bones pràctiques de tot el mòdul 7 i amb un caixa.py que ajunta les tres peces: try/except, excepcions pròpies i logging.
Contingut
- Per què
print()no serveix per diagnosticar - Els cinc nivells: de
DEBUGaCRITICAL logging.basicConfig: format i sortida a fitxer- Un logger per mòdul:
getLogger(__name__) logger.exception(): el traceback, al log- Què registrar i què no
- Decàleg del mòdul 7 i el
caixa.pyrobust
Per què print() no serveix per diagnosticar
print() i logging responen preguntes diferents: print() parla amb l'usuari que hi ha al davant; el log parla amb el desenvolupador que arribarà després. Tan bon punt el programa viu més d'una sessió, print() com a eina de diagnòstic falla per tot arreu:
| Necessitat en producció | print() |
logging |
|---|---|---|
| Que quedi rastre en tancar el terminal | No: s'esfuma | Sí: fitxer persistent |
| Saber quan ha passat cada cosa | Caldria concatenar la data a mà a cada print | Timestamp automàtic al format |
| Distingir rutina d'emergència | Tot s'imprimeix igual | Nivells: INFO vs ERROR |
| Apujar/abaixar el detall sense tocar codi | Afegir/esborrar prints per tot el fitxer | Canviar level= en un sol lloc |
| Desar el traceback d'una excepció gestionada | Copiar-lo a mà (ningú no ho fa) | logger.exception() ho fa tot sol |
| Saber quin mòdul ha parlat | Anònim | El nom del logger va a cada línia |
Ull al matís: print() no queda prohibit. El menú de la caixa, el "Venda feta: 20.70 €", els missatges a la Júlia i l'Omar — això és interfície d'usuari i continuarà sent print() (fins que el mòdul 10 ho converteixi en web). El que emigra al log és la telemetria: avisos, errors, molles de pa per al diagnòstic.
Els cinc nivells: de DEBUG a CRITICAL
Cada missatge de log porta un nivell de gravetat. Són cinc, ordenats, i el logger té un llindar: només registra el que iguala o supera el seu nivell configurat.
| Nivell | Quan fer-lo servir (criteri Papyrus) | Exemple |
|---|---|---|
DEBUG |
Detall fi per desenvolupar; en producció sol anar apagat | clau normalitzada: "l'odissea" |
INFO |
La vida normal de la botiga: fites que confirmen que tot va bé | "Cataleg carregat: 4 llibres" / "Venda: Hamlet x2, 20.70 €" |
WARNING |
Alguna cosa estranya però el programa continua; algú hi hauria de fer un cop d'ull | "Fila 7 de vendes.csv no llegible, ignorada" |
ERROR |
Una operació ha fallat; el programa sobreviu però aquella acció no s'ha fet | "No s'ha pogut desar el cataleg" |
CRITICAL |
El programa no pot continuar | "cataleg.json corrupte i sense copies a copies/" |
La prova del cotó per triar nivell: què hauria de fer qui ho llegeixi? Res, només context → DEBUG/INFO. Fer-hi un cop d'ull aquesta setmana → WARNING. Actuar avui → ERROR. Deixar el que estigui fent i córrer → CRITICAL. Amb el llindar a INFO (típic de producció), els DEBUG es descarten sense cost; baixes a DEBUG quan persegueixes una fallada, i el mateix codi es torna loquaç sense afegir ni un sol print.
logging.basicConfig: format i sortida a fitxer
Una sola crida, al principi del programa principal i només una vegada, configura el sistema sencer:
# caixa.py — en arrencar, abans de cap altra cosa
import logging
from pathlib import Path
BASE = Path(__file__).parent # l'ancora del modul 6
(BASE / "dades").mkdir(exist_ok=True)
logging.basicConfig(
filename=BASE / "dades" / "papyrus.log",
level=logging.INFO,
format="%(asctime)s %(levelname)-8s %(name)s: %(message)s",
encoding="utf-8",
)Peça a peça:
filename: els missatges van adades/papyrus.logen comptes de a pantalla. S'obre en mode append (a, del mòdul 6): cada arrencada suma, no aixafa — l'historial és justament el valor.level=logging.INFO: el llindar.DEBUGes descarta; d'INFOcap amunt, s'escriu.format: la plantilla de cada línia.%(asctime)sés la data i l'hora,%(levelname)-8sel nivell alineat a 8 caràcters,%(name)sel logger que parla,%(message)sel teu text. (És la sintaxi clàssica de%, anterior a les f-strings;loggingla conserva per història i rendiment.)encoding="utf-8": l'innegociable del mòdul 6 també regeix aquí — al log hi haurà títols amb accents.
El resultat, tal com es veu dins de papyrus.log:
2026-07-13 09:00:12,451 INFO magatzem: Cataleg carregat: 4 llibres
2026-07-13 09:14:03,102 INFO magatzem: Venda: Hamlet x2, 20.70 €
2026-07-13 09:31:44,876 WARNING caixa: Fila ignorada a vendes.csv: {'data': '2026-07-12', 'titol': 'Faust', 'quantia': 'gratis'}Data, gravetat, mòdul, missatge amb la dada culpable: el dilluns al matí, l'Anna (o tu) reconstrueix el dissabte línia a línia.
Un logger per mòdul: getLogger(__name__)
No es crida logging.info(...) a pèl per tot el codi. El patró professional crea un logger amb nom a la capçalera de cada mòdul:
# magatzem.py
import logging
logger = logging.getLogger(__name__) # nom: "magatzem"
def carregar_cataleg():
...
logger.info("Cataleg carregat: %d llibres", len(cataleg))__name__és la variable que el mòdul 3 ens va presentar (la de l'if __name__ == "__main__":): val"magatzem"quan el mòdul s'importa. Així, cada línia del log declara el seu origen — la columna%(name)sdel format — sense esforç.- Divisió de poders: els mòduls (
magatzem.py,errors.py...) emeten amb el seu logger; només el programa principal configura ambbasicConfig. Una biblioteca que configurés el logging trepitjaria la configuració de qui la importa. - L'estil
%damb arguments separats (logger.info("...: %d llibres", len(cataleg))) deixa queloggingcompongui el text només si el missatge passa el llindar. Amb f-strings també funciona i és acceptable mentre aprens; l'estil diferit és l'idiomàtic.
logger.exception(): el traceback, al log
La joia de la corona. Dins d'un except, logger.exception(...) registra el missatge a nivell ERROR i hi afegeix el traceback complet de l'excepció en curs — l'informe forense de 07-01, arxivat automàticament:
def desar_cataleg(cataleg):
ruta = BASE / "dades" / "cataleg.json"
try:
with open(ruta, "w", encoding="utf-8") as f:
json.dump([asdict(l) for l in cataleg.values()], f,
ensure_ascii=False, indent=2)
except OSError:
logger.exception("No s'ha pogut desar el cataleg a %s", ruta)
raise # registrar NO es gestionar: que decideixi qui ha cridatA papyrus.log apareix la línia ERROR seguida del Traceback (most recent call last): ... íntegre, amb fitxer, línia i causa. Tres regles d'ús:
- Només dins d'un
except— és on existeix "l'excepció en curs" que cal adjuntar. - Combina perfectament amb el
raisea seques de 07-03: registrar a la frontera i rellançar. El log guarda l'evidència; l'excepció continua el seu viatge cap a qui pugui decidir. Registrar no és gestionar. - Substitueix l'antipatró definitiu:
except OSError: passera silenci culpable (07-02);except OSError: logger.exception(...)seguit d'una decisió conscient (rellançar, valor per defecte, avortar) és enginyeria.
Què registrar i què no
Un log útil explica la història operativa del programa. Registra: arrencada i aturada (amb versió i configuració carregada), fites de negoci a INFO (vendes, altes, reposicions, tancaments de caixa), tota anomalia a WARNING+ amb la dada culpable ({fila!r}, la ruta, el títol), i el traceback via logger.exception() a cada except fronterer.
I el que no hi ha d'entrar, perquè el log és un fitxer de text pla que es copia, es comparteix i acaba en correus de suport:
- Dades personals i sensibles: res de noms complets de clients associats a les seves compres, adreces, telèfons, dades bancàries o de targetes.
"Venda: Hamlet x1, soci MARTA-002"val (un codi intern opac);"Marta García, targeta 4779..."mai. La legislació de protecció de dades (a Europa, el RGPD) converteix això en obligació legal, no només en cortesia — i un log no té el control d'accés d'una base de dades. - Secrets: contrasenyes, tokens, claus d'API. Ni tan sols "per depurar un moment": els logs s'arxiven i els secrets que s'hi escriuen queden compromesos.
- Soroll: registrar cada iteració d'un bucle a
INFOenterra el senyal. Per a això existeixDEBUG, que en producció va apagat.
Decàleg del mòdul 7 i el caixa.py robust
Les deu regles que resumeixen tot el mòdul, cadascuna amb la seva lliçó d'origen:
- Llegeix el traceback de baix a dalt: tipus i missatge primer, cadena de crides després (07-01).
- EAFP davant del món exterior: intenta i gestiona;
Path.exists()era un pal·liatiu amb condició de carrera (07-01). - Captura tipus concrets: mai un
except:nu;Exceptionnomés a l'última frontera, i registrant (07-02). trymínim: només la línia que pot fallar;elseper al camí feliç,finallyper a la neteja incondicional (07-02).- No silenciïs: un error atrapat es compta, es registra o es rellança; mai
passa seques (07-02, 07-05). - Falla aviat: guard clauses a la porta, mutació de l'estat només després de superar totes les guardes (07-03).
- Missatges accionables: què s'esperava, què ha arribat (
!r) i, si escau, què cal fer (07-03). Noneper a absències legítimes, excepció per a contractes trencats; si conviuen, que sigui amb criteri, comcercar_llibre()/obtenir_llibre()(07-03, 07-04).- Jerarquia pròpia amb atributs: l'
exceptdistingeix el teu error de la resta i el codi llegeix dades, no strings (07-04). - Log a la frontera: registra amb
logger.exception()on gestiones, no a cada nivell intermedi — un error, una entrada al log, no cinc (07-05).
I el tancament pràctic: caixa.py ajuntant les tres peces del mòdul.
# caixa.py — el programa que l'Anna executa cada mati
import logging
from pathlib import Path
from magatzem import carregar_cataleg, desar_cataleg, vendre
from errors import ErrorPapyrus, LlibreNoTrobatError, EstocInsuficientError
BASE = Path(__file__).parent
(BASE / "dades").mkdir(exist_ok=True)
logging.basicConfig(
filename=BASE / "dades" / "papyrus.log",
level=logging.INFO,
format="%(asctime)s %(levelname)-8s %(name)s: %(message)s",
encoding="utf-8",
)
logger = logging.getLogger(__name__)
def demanar_enter(missatge): # el deute de M1, saldat a 07-02
while True:
text = input(missatge)
try:
return int(text)
except ValueError:
print(f"'{text}' no es un nombre enter. Torna-ho a provar.")
def main():
logger.info("Papyrus obre la caixa")
cataleg = carregar_cataleg() # EAFP per dins (07-02)
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: # fulla: reaccio especifica (07-04)
print(f"No tenim '{e.titol}'. Vols que l'encarreguem?")
logger.warning("Venda fallida, titol desconegut: %r", e.titol)
except EstocInsuficientError as e: # fulla: atributs llegibles (07-04)
print(f"Nomes en queden {e.disponible} de '{e.titol}'.")
logger.warning("Estoc curt: %s", e)
except ErrorPapyrus as e: # base: xarxa del negoci (07-04)
print(f"Operacio no feta: {e}")
logger.error("Error de negoci no especific: %s", e)
else: # cami felic fora del try (07-02)
print(f"Venda feta: {quantia:.2f} € (amb IVA)")
logger.info("Venda: %s x%d, %.2f €", titol, unitats, quantia)
desar_cataleg(cataleg)
logger.info("Papyrus tanca la caixa")
if __name__ == "__main__":
try:
main()
except Exception: # UNICA frontera amb Exception (regla 3)
logger.exception("Error no previst: la caixa es tanca d'emergencia")
print("Hi ha hagut un error inesperat. Consulta dades/papyrus.log")
raise SystemExit(1)Fixa't en l'arquitectura de tres anells: les fulles de la jerarquia es gestionen a tocar del taulell (reacció específica, WARNING), la base ErrorPapyrus fa de xarxa del negoci, i un únic except Exception a la porta del programa — amb logger.exception() i un missatge honest a pantalla — caça l'imprevist sense silenciar-lo. print() per a l'Anna, logging per al diagnòstic: cada missatge, al seu públic.
Queda una menció honrada: en sistemes que corren setmanes, papyrus.log creix sense límit. El mateix logging porta la solució — handlers com RotatingFileHandler, que parteix el log per mida o per data, i configuració des de fitxer — però és tema avançat que excedeix l'essencial; en tindràs prou sabent que existeix i on buscar-lo (logging.handlers) quan la botiga ho necessiti.
Errors Comuns i Consells
- Cridar
basicConfig()a cada mòdul. Només el programa principal configura; els mòduls creen el seugetLogger(__name__)i emeten. A més,basicConfignomés té efecte la primera vegada: les crides repetides s'ignoren en silenci i confonen. logger.error(e)dins de l'excepten comptes delogger.exception(...). Desa el missatge però llença el traceback, que era la meitat valuosa. Dins d'except, gairebé sempreexception().- Registrar i creure que ja està gestionat. El log és memòria, no decisió: després de registrar, decideix — rellança, aplica un valor per defecte o avorta. Registrar-i-empassar és l'antipatró
passamb millor vestuari. - Registrar el mateix error a cada nivell de la pila. Cinc entrades idèntiques per una sola fallada embruten el diagnòstic. Regla 10: es registra a la frontera on es gestiona.
- Dades sensibles al log. Revisa cada
logger.*com si el fitxer l'hagués de llegir un desconegut — perquè tard o d'hora ho farà. Codis interns sí; identitats, targetes i secrets, mai. - Consell: durant el desenvolupament, canvia el llindar a
level=logging.DEBUGi afegeixDEBUGgenerosos a la lògica delicada (normalització de claus, conversions). En producció, llindar aINFO: elsDEBUGcontinuen allà, adormits i gratis. - Consell: obre
papyrus.logde tant en tant encara que res no falli. Un log que només es llegeix a les emergències sol descobrir-se buit, mal formatat o mut just quan més falta fa.
Exercicis
-
tancament_de_caixa()amb telemetria. Adapta la versió de 07-02 perquè faci servir un logger de mòdul: cada fila corrupta aWARNINGamb la fila completa (%r), el total del dia aINFO, i davant deFileNotFoundErrorunINFO("sense vendes avui") — per quèINFOi noERROR? Elsprint()de resum per a l'Anna es mantenen: raona quin missatge pertany a cada canal. -
La còpia nocturna que avisa. Embolica la
copia_de_seguretat()del mòdul 6 (que copiacataleg.jsonacopies/cataleg-<data>.jsonambshutil.copy2) perquè: registri aINFOla destinació si tot va bé; i davant d'OSErrorfaci servirlogger.exception()i rellanci. Explica per què rellançar en comptes d'empassar-se l'error, sent "només" una còpia. -
Auditoria del decàleg. Aquest fragment infringeix com a mínim quatre regles del decàleg. Identifica-les pel número i reescriu-lo:
def alta_soci(socis, nom, codi): try: logging.basicConfig(level=logging.DEBUG) if codi in socis: return False socis.append(codi) logging.info("Alta de soci: " + nom + " amb targeta 4779-1234-5678-9010") return True except: pass
Solucions
-
logger = logging.getLogger(__name__) def tancament_de_caixa(): ruta = BASE / "dades" / "vendes.csv" total, vendes_ok, 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"]) except ValueError: corruptes += 1 logger.warning("Fila no llegible a vendes.csv: %r", fila) else: vendes_ok += 1 except FileNotFoundError: logger.info("Tancament sense vendes: no existeix %s", ruta) print("Avui no hi ha vendes registrades.") return 0.0 logger.info("Tancament de caixa: %d vendes, %d corruptes, total %.2f €", vendes_ok, corruptes, total) print(f"Tancament de caixa: {vendes_ok} vendes, total {total:.2f} €") return totalINFOi noERRORperquè un dia sense vendes és un resultat legítim del negoci (el criteri de 07-03 aplicat a nivells de log): ningú no ha d'"actuar avui". Canals: elprintdona a l'Anna el resum que necessita en el moment; el log guarda a més les files corruptes exactes i el desglossament, que és el que el diagnòstic del dilluns necessitarà. -
def copia_de_seguretat(): origen = BASE / "dades" / "cataleg.json" destinacio = BASE / "copies" / f"cataleg-{date.today().isoformat()}.json" try: shutil.copy2(origen, destinacio) except OSError: logger.exception("Copia de seguretat fallida cap a %s", destinacio) raise logger.info("Copia de seguretat creada: %s", destinacio)Es rellança perquè una còpia fallida silenciada és una falsa sensació de seguretat: el dia que l'Anna necessiti restaurar des de
copies/, descobriria que fa setmanes que no es creen. Regla 5 (no silenciar) i el patró registrar-i-rellançar de 07-03/07-05: el log conserva el traceback i qui ha cridat decideix si avortar el tancament o avisar a crits. -
Infraccions: regla 3 (
except:nu), regla 5 (pass: l'error desapareix, i a sobre la funció retornaNoneen comptes deTrue/False, un tercer estat fantasma), regla 10 / patró de configuració (basicConfigdins d'una funció de biblioteca, trepitjant — o sent ignorada per — la configuració del programa principal), i la norma de dades sensibles (un número de targeta escrit al log). De propina: logger anònim en comptes degetLogger(__name__)i untryque abriga codi que no pot fallar de manera esperable (regla 4). Reescriptura:logger = logging.getLogger(__name__) def alta_soci(socis, nom, codi): if not nom.strip() or not codi.strip(): raise ValueError("nom i codi no poden estar buits") # regla 6 if codi in socis: logger.info("Alta rebutjada, codi duplicat: %r", codi) return False socis.append(codi) logger.info("Alta de soci: %s", codi) # codi intern; ni nom ni targeta return True
Conclusió
El mòdul 7 respon, una a una, les preguntes que va deixar obertes el tancament del mòdul 6. I si el fitxer no hi és? carregar_cataleg() ho intenta a l'estil EAFP i converteix el FileNotFoundError en un catàleg buit amb avís — sense el pal·liatiu Path.exists() ni la seva condició de carrera (07-01, 07-02). I si la còpia és corrupta? JSONDecodeError es distingeix del fitxer absent i es tradueix amb raise ... from e sense perdre l'evidència (07-02, 07-03). I si float(fila["preu"]) rep "gratis"? Un try mínim salta la fila, la compta i la deixa escrita a WARNING a papyrus.log (07-02, 07-05). I si la Júlia tecleja un preu impossible? Les guard clauses i el __post_init__ de M5 — que ara saps llegir com a contractes — el rebutgen amb un missatge accionable, i demanar_enter() va saldar de passada el deute que int(input()) arrossegava des del mòdul 1 (07-03, 07-02). Pel camí, Papyrus ha guanyat un vocabulari propi d'errors — ErrorPapyrus i les seves fulles amb atributs, a errors.py — i una caixa de tres anells que atrapa l'específic a tocar del taulell, el genèric a la frontera i ho deixa tot anotat amb logger.exception() (07-04, 07-05). La botiga ja no cau: s'explica. El mòdul 8 canvia de terç: el codi de Papyrus és correcte i robust, i ara toca fer-lo més expressiu i més eficient — type hints que documenten els contractes que aquest mòdul defensava a mà, decoradors que emboliquen funcions sense repetir codi, generadors que processen vendes sense carregar-les senceres a memòria, i el moment de saldar la promesa pendent del with: entendre els context managers per dins i, per fi, construir els teus.
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
