Tres lliçons de persistència han deixat la carpeta de Papyrus feta un basar: registre_altes.txt, cataleg.csv, vendes.csv, cataleg.json, socis.json, config.json… tot amuntegat al costat dels .py. I hi ha feines pendents que no són de contingut sinó de gestió: crear carpetes, comprovar què existeix, llistar els informes d'un mes, canviar noms en lot, copiar el catàleg a una còpia de seguretat amb la data al nom. De tot això se n'ocupa aquesta lliçó, amb pathlib.Path com a protagonista — la manera moderna i orientada a objectes de treballar amb rutes —, el mòdul os que el mòdul 3 va deixar promès (avui entendràs per què el vam ajornar), i shutil per copiar i moure. Al final, Papyrus tindrà una estructura de projecte professional i una còpia de seguretat nocturna automàtica. El mòdul tanca on va començar: l'Anna apaga l'ordinador — i ja no perd res.
Contingut
- Rutes: la dada que sembla una cadena però no ho és
pathlib.Path: rutes orientades a objectes- Rutes relatives vs absolutes i
Path.cwd() - Anatomia d'una ruta:
name,stem,suffix,parent - Preguntar i crear:
exists(),is_file(),is_dir(),mkdir() - Cercar fitxers amb
glob() osios.path: el llegat (taula d'equivalències)shutil: copiar, moure i esborrar arbres- L'estructura definitiva del projecte
papyrus/ - Còpia de seguretat nocturna amb data
- Canvi de nom en lot: els informes diaris
- Errors comuns i consells
- Exercicis amb solucions
Rutes: la dada que sembla una cadena però no ho és
Fins ara hem passat a open() cadenes com "cataleg.json", i per al fitxer solt al costat de l'script n'hi havia prou. Però una ruta real té estructura (carpetes, nom, extensió), operacions pròpies (unir, pujar un nivell, canviar l'extensió) i un problema incòmode: el separador canvia segons el sistema — \ a Windows, / a macOS/Linux. Manipular rutes concatenant cadenes (carpeta + "\\" + nom) produeix codi fràgil i no portable. Python té des de sempre os.path per a això (funcions sobre cadenes) i, des de fa anys, una cosa millor: tractar la ruta com un objecte amb mètodes — exactament la filosofia del mòdul 5.
pathlib.Path: rutes orientades a objectes
Path (del mòdul pathlib, biblioteca estàndard) ja va fer un cameo a 06-01 amb exists(); ara li toca el paper principal:
from pathlib import Path
dades = Path("dades") # una ruta relativa (encara no toca el disc)
cataleg = dades / "cataleg.json" # l'operador / uneix rutes — en qualsevol sistema!
print(cataleg) # dades\cataleg.json (Windows) — dades/cataleg.json (macOS/Linux)L'estrella és l'operador /: Path sobrecarrega la divisió (un mètode màgic més, __truediv__ — 05-05 en acció a la biblioteca estàndard) per unir trams de ruta amb el separador correcte de cada sistema. Mai més "dades" + "\\" + "cataleg.json".
Crear un Path no crea res al disc: és un valor que descriu una ruta, existeixi o no. I encaixa amb tot el que has après: open() (i per tant csv i json) accepta objectes Path on abans passàvem cadenes — fins i tot té dreceres que ja coneixes implícitament:
with open(cataleg, "r", encoding="utf-8") as f: # un Path on anava una cadena: identic
...
text = cataleg.read_text(encoding="utf-8") # drecera: llegeix tot (equival a open+read)
cataleg.write_text(text, encoding="utf-8") # drecera: escriu tot (open "w"+write)Les dreceres read_text/write_text són còmodes per a fitxers petits d'una sola operació; per a tota la resta, el with open(...) de 06-01 segueix sent el patró.
Rutes relatives vs absolutes i Path.cwd()
- Una ruta absoluta parteix de l'arrel del sistema:
C:\botigues\papyrus\dades\cataleg.json. Inequívoca, però lligada a una màquina concreta. - Una ruta relativa (
dades/cataleg.json) es resol des del directori de treball actual (current working directory), que consultes ambPath.cwd().
print(Path.cwd()) # C:\botigues\papyrus (des d'on has executat python)
print((Path("dades") / "cataleg.json").resolve()) # la versio absoluta de la relativaLa trampa clàssica: el directori de treball és des d'on executes, no on és l'script. Si executes python papyrus\caixa.py des de C:\botigues, les rutes relatives del programa es resolen des de C:\botigues — i de sobte "no troba" un dades/ que sí que existeix, però un nivell més avall. L'antídot professional és ancorar les rutes al mateix fitxer amb la variable __file__ (cosina de la __name__ de 03-04):
BASE = Path(__file__).parent # la carpeta on viu AQUEST script
DADES = BASE / "dades" # sempre correcta, s'executi des d'on s'executiAquest parell de línies encapçalarà els programes de Papyrus a partir d'ara.
Anatomia d'una ruta: name, stem, suffix, parent
Un Path es deixa disseccionar amb atributs de només lectura:
ruta = Path("dades") / "copies" / "cataleg-2026-07-13.json"
print(ruta.name) # cataleg-2026-07-13.json (nom complet)
print(ruta.stem) # cataleg-2026-07-13 (nom sense extensio)
print(ruta.suffix) # .json (l'extensio, amb punt)
print(ruta.parent) # dades\copies (la carpeta contenidora, un altre Path)I per derivar rutes noves sense tocar cadenes: ruta.with_name("altre.json") canvia el nom complet i ruta.with_suffix(".csv") només l'extensió. Aquestes quatre peces — stem, suffix, with_name, glob — són el kit complet del canvi de nom en lot que faràs al final.
Preguntar i crear: exists(), is_file(), is_dir(), mkdir()
DADES = Path("dades")
print(DADES.exists()) # hi ha res en aquesta ruta?
print(DADES.is_dir()) # i es una carpeta?
print((DADES / "cataleg.json").is_file()) # es un fitxer?
DADES.mkdir(parents=True, exist_ok=True) # crea la carpeta — la linia canonicaEls dos arguments de mkdir() mereixen paràgraf propi, perquè sense ells el mètode és rondinaire:
parents=True: crea també les carpetes intermèdies que faltin (dades/copies/2026d'una tirada). Sense ell, si falta un tram intermedi:FileNotFoundError.exist_ok=True: no protesta si la carpeta ja existeix. Sense ell, segona execució del programa:FileExistsError.
Junts fan mkdir idempotent — pots cridar-lo mil vegades amb el mateix resultat —, ideal per a l'arrencada d'un programa: "assegura't que les meves carpetes existeixen". I sí: exists() és el pal·liatiu que fem servir des de 06-01 per a fitxers que potser no hi són; segueix sent un pedaç amb esquerdes, i el mòdul 7 (ja imminent) porta l'eina de veritat.
Cercar fitxers amb glob()
glob(patro) retorna els fitxers d'una carpeta que encaixen amb un patró de comodins — * significa "qualsevol cosa":
INFORMES = Path("informes")
for ruta in sorted(INFORMES.glob("*.csv")): # tots els CSV d'informes/
print(ruta.name)
juliol = sorted(INFORMES.glob("informe-2026-07-*.csv")) # nomes els de juliol de 2026
print(len(juliol), "informes aquest mes")Detalls d'ús: retorna objectes Path (amb tots els seus atributs a punt), en ordre no garantit — d'aquí el sorted() —, i amb rglob(patro) la cerca descendeix recursivament per les subcarpetes. Fixa't en el dividend de posar noms amb dates ISO (yyyy-mm-dd, com les LiniaVenda de 05-06): els noms ordenen cronològicament per si sols i els patrons per mes o per any surten gratis.
os i os.path: el llegat (taula d'equivalències)
El mòdul 3 va esmentar os («interacció amb el sistema operatiu») i va prometre desenvolupar-lo aquí. Complert — amb matís: per a rutes i fitxers, os/os.path són l'API veterana, anterior a pathlib, basada en funcions que reben i retornen cadenes. La trobaràs en milions de línies de codi existent i en respostes antigues d'internet, així que has de saber llegir-la; per escriure codi nou, aquest curs fa servir pathlib.
| Tasca | os / os.path (llegat) |
pathlib (modern) |
|---|---|---|
| Unir rutes | os.path.join("dades", "cataleg.json") |
Path("dades") / "cataleg.json" |
| Existeix? | os.path.exists(ruta) |
ruta.exists() |
| És fitxer / carpeta? | os.path.isfile(r) / os.path.isdir(r) |
r.is_file() / r.is_dir() |
| Crear carpetes | os.makedirs(r, exist_ok=True) |
r.mkdir(parents=True, exist_ok=True) |
| Nom / extensió | os.path.basename(r) / os.path.splitext(r)[1] |
r.name / r.suffix |
| Carpeta contenidora | os.path.dirname(r) |
r.parent |
| Directori de treball | os.getcwd() |
Path.cwd() |
| Llistar contingut | os.listdir(r) (noms, cadenes) |
r.iterdir() / r.glob("*") (objectes Path) |
| Canviar nom/moure | os.rename(a, b) |
a.rename(b) |
| Esborrar un fitxer | os.remove(r) |
r.unlink() |
La diferència de fons és la del mòdul 5: os.path són funcions soltes operant sobre cadenes passives; pathlib empaqueta dades i comportament en un objecte. La mateixa evolució que va viure el catàleg. (os segueix sent insubstituïble per a altres coses — variables d'entorn amb os.environ, processos — que de moment queden fora d'aquest curs.)
shutil: copiar, moure i esborrar arbres
pathlib no copia fitxers; per a això hi ha shutil (shell utilities), el seu company natural:
import shutil
shutil.copy2("dades/cataleg.json", "copies/cataleg.json") # copia fitxer (amb metadades)
shutil.move("informe.csv", "informes/informe.csv") # mou (o canvia el nom entre carpetes)
shutil.copytree("dades", "dades_mirall") # copia una carpeta SENCERA
shutil.rmtree("dades_mirall") # ⚠ esborra un arbre COMPLETcopy2()copia el fitxer conservant metadades com la data de modificació (la seva germanacopy()no les conserva; per a còpies de seguretat,copy2).move()serveix tant per moure entre carpetes com per canviar noms.rmtree()mereix lletres vermelles: esborra la carpeta i tot el seu contingut, recursivament, sense paperera de reciclatge i sense confirmació. Unrmtreesobre la ruta equivocada és irrecuperable. Regla d'or: mai sobre una ruta construïda amb dades de l'usuari, i sempre després d'unis_dir()sobre una ruta que puguis llegir en veu alta sense suar.
L'estructura definitiva del projecte papyrus/
Amb carpetes, rutes ancorades i mkdir idempotent, endrecem per fi el basar:
papyrus/ ├── caixa.py ← el programa principal ├── fitxa_llibre.py ← altes (06-01) ├── papyrus_utils.py ← el modul propi de 03-04 ├── config.json ← configuracio (06-03), al costat del codi ├── dades/ ← l'estat viu de la botiga │ ├── cataleg.json ← el cataleg canonic (06-03) │ ├── vendes.csv ← historic de vendes (06-02) │ ├── socis.json ← socis i compres (06-03) │ └── registre_altes.txt ← dietari d'altes (06-01) ├── copies/ ← copies de seguretat datades └── informes/ ← informes diaris de caixa (06-02)
I l'arrencada que la garanteix, a punt per encapçalar caixa.py:
from pathlib import Path
BASE = Path(__file__).parent # ancorat a l'script, no al cwd
DADES = BASE / "dades"
COPIES = BASE / "copies"
INFORMES = BASE / "informes"
for carpeta in (DADES, COPIES, INFORMES):
carpeta.mkdir(parents=True, exist_ok=True) # idempotent: mil execucions, un resultat
RUTA_CATALEG = DADES / "cataleg.json"
RUTA_VENDES = DADES / "vendes.csv"
RUTA_CONFIG = BASE / "config.json"A partir d'aquí, carregar_cataleg_json(RUTA_CATALEG) i companyia (06-02/06-03) reben objectes Path i tot funciona igual — però endreçat i portable.
Còpia de seguretat nocturna amb data
L'Anna ja no perd el catàleg en apagar… però i si un dia es desa un catàleg corromput a sobre del bo? Un sol fitxer és un sol punt de fallada. La solució clàssica: al tancament de caixa, copiar el catàleg a copies/ amb la data al nom — datetime (03-05) fabrica el nom i shutil.copy2 fa la feina:
import shutil
from datetime import date
from pathlib import Path
def copia_de_seguretat(ruta_cataleg, carpeta_copies):
"""Copia datada del cataleg. Retorna la ruta de la copia, o None si no hi ha res a copiar."""
if not ruta_cataleg.exists(): # pedac conegut; el modul 7 el jubilara
return None
carpeta_copies.mkdir(parents=True, exist_ok=True)
desti = carpeta_copies / f"{ruta_cataleg.stem}-{date.today()}{ruta_cataleg.suffix}"
shutil.copy2(ruta_cataleg, desti)
return desti
print(copia_de_seguretat(RUTA_CATALEG, COPIES)) # copies\cataleg-2026-07-13.jsonDisecciona la línia del desti: stem (cataleg) + data ISO + suffix (.json) = cataleg-2026-07-13.json. Cada nit, un fitxer nou; cap còpia no trepitja l'anterior; i sorted(COPIES.glob("cataleg-*.json"))[-1] — glob + ordre ISO — retorna sempre la còpia més recent. Tres lliçons d'aquest mòdul col·laborant en una expressió.
Canvi de nom en lot: els informes diaris
Últim encàrrec de l'Anna. Abans d'automatitzar la caixa, els informes es desaven a mà com informe-13-07-2026.csv — data "a l'europea", que ordenada alfabèticament barreja els mesos (el 13-07 queda abans que el 02-08... i després que l'01-01 de qualsevol any). N'hi ha dotzenes. Canviar-los de nom a mà, ni parlar-ne: glob + stem + rename ho fan en lot:
from pathlib import Path
def normalitzar_informes(carpeta):
"""informe-dd-mm-aaaa.csv → informe-aaaa-mm-dd.csv (ordre cronologic gratis)."""
for ruta in carpeta.glob("informe-??-??-????.csv"): # ? = exactament un caracter
_, dia, mes, any_ = ruta.stem.split("-") # any_ per no trepitjar el builtin any()
nova = ruta.with_name(f"informe-{any_}-{mes}-{dia}{ruta.suffix}")
if nova.exists(): # no trepitjar: rename sobreescriu sense avisar
print(f"AVIS: {nova.name} ja existeix; no toco {ruta.name}")
continue
ruta.rename(nova)
print(f"{ruta.name} → {nova.name}")
normalitzar_informes(INFORMES)
# informe-13-07-2026.csv → informe-2026-07-13.csv
# informe-01-07-2026.csv → informe-2026-07-01.csvEl patró informe-??-??-????.csv (cada ? casa amb un únic caràcter) evita capturar els informes ja normalitzats — que tenen l'any, de quatre xifres, just després del guió. És la recepta general del canvi de nom en lot: glob per seleccionar, stem/split per desmuntar el nom, with_name per muntar-lo, exists per no trepitjar, rename per executar. Memoritza-la: la faràs servir amb fotos, baixades i logs la resta de la teva vida.
Errors Comuns i Consells
- Concatenar rutes com a cadenes (
carpeta + "/" + nom): fràgil i no portable. L'operador/dePathexisteix per a això i tria el separador correcte a cada sistema. - Confiar en el directori de treball: "al meu terminal funcionava" és el símptoma. Les rutes relatives depenen de des d'on s'executa; ancora amb
BASE = Path(__file__).parent. mkdir()pelat: senseparents=Truefalla si falta un tram; senseexist_ok=Truefalla a la segona execució. La forma canònica porta tots dos, gairebé sempre.- Oblidar que
glob()no ordena: l'ordre depèn del sistema de fitxers. Si l'ordre importa — i amb noms datats gairebé sempre importa — embolcalla ambsorted(). rename()cap a un nom que ja existeix: en alguns sistemes sobreescriu silenciosament el destí. Comprovaexists()abans, com anormalitzar_informes().rmtree()alegre: esborra arbres sencers sense paperera. Verifica la ruta (imprimeix-la abans en una execució de prova), no la construeixis mai amb entrada de l'usuari, i pregunta't simove()a una carpetapaperera/pròpia no és millor política.- Rutes de Windows en cadenes amb
\:"dades\nou.txt"amaga un\n(salt de línia!, 01-03). Si escrius rutes literals, fes servir/— Windows també l'accepta — o millor, construeix-les ambPathi l'operador/. - Consell: posa als fitxers datats noms sempre en ISO (
aaaa-mm-dd). Ordre alfabètic = ordre cronològic, elsglobper mes surten sols i t'estalvies el canvi de nom en lot que acabes d'haver de fer.
Exercicis
Exercici 1: inventari del projecte
Escriu inventari(base) que rebi la ruta de papyrus/ i recorri dades/, copies/ i informes/ imprimint, per carpeta, cada fitxer amb la seva extensió i la seva mida en bytes (ruta.stat().st_size), i al final el total de fitxers. Fes servir glob("*") i is_file() per ignorar possibles subcarpetes. Extra: agrupa els totals per suffix amb un Counter (04-06).
Exercici 2: retenció de còpies
Les còpies nocturnes s'acumulen sense límit. Escriu netejar_copies(carpeta, conservar=7) que llisti les cataleg-*.json de copies/, les ordeni per nom (data ISO: l'ordre alfabètic és cronològic) i esborri amb unlink() totes menys les conservar més recents, imprimint cada esborrat. Prova-la creant a mà deu còpies falses amb write_text().
Exercici 3: tancament de caixa complet
Ajunta el mòdul sencer a tancament_de_caixa(base): (1) assegura les carpetes amb la línia canònica de mkdir; (2) desa el catàleg amb desar_cataleg_json() (06-03) a dades/cataleg.json; (3) fes la còpia datada amb copia_de_seguretat(); (4) genera informes/informe-<avui>.csv amb informe_caixa() (06-02) llegint dades/vendes.csv; i (5) imprimeix un resum amb els noms dels fitxers creats. Executa-la dues vegades seguides i comprova que no falla ni duplica res d'estrany.
Solucions
Solució 1:
from collections import Counter
from pathlib import Path
def inventari(base):
total = 0
per_tipus = Counter()
for carpeta in (base / "dades", base / "copies", base / "informes"):
print(f"\n{carpeta.name}/")
for ruta in sorted(carpeta.glob("*")):
if ruta.is_file(): # ignora subcarpetes
print(f" {ruta.name:<32} {ruta.suffix:<6} {ruta.stat().st_size} bytes")
per_tipus[ruta.suffix] += 1
total += 1
print(f"\nTotal: {total} fitxers — {dict(per_tipus)}")
inventari(BASE)stat() dona accés a les metadades del fitxer (mida, dates); st_size és la mida en bytes.
Solució 2:
def netejar_copies(carpeta, conservar=7):
copies = sorted(carpeta.glob("cataleg-*.json")) # ISO ⇒ ordre alfabetic = cronologic
for ruta in copies[:-conservar]: # totes menys les N ultimes (04-01)
ruta.unlink()
print(f"Esborrada copia antiga: {ruta.name}")
# banc de proves: deu copies falses
for dia in range(1, 11):
(COPIES / f"cataleg-2026-07-{dia:02d}.json").write_text("{}", encoding="utf-8")
netejar_copies(COPIES) # esborra de la 01 a la 03; queden les 7 mes recentsL'slicing negatiu de 04-01 fa la política de retenció en una línia. Fixa't en {dia:02d}: sense el zero de farciment (cataleg-2026-07-1.json), l'ordre alfabètic deixaria de ser cronològic i la neteja esborraria les còpies equivocades.
Solució 3:
from datetime import date
def tancament_de_caixa(base):
dades, copies, informes = base / "dades", base / "copies", base / "informes"
for carpeta in (dades, copies, informes):
carpeta.mkdir(parents=True, exist_ok=True) # (1) idempotent
ruta_cataleg = dades / "cataleg.json"
desar_cataleg_json(cataleg, ruta_cataleg) # (2) 06-03
copia = copia_de_seguretat(ruta_cataleg, copies) # (3) aquesta llico
ruta_informe = informes / f"informe-{date.today()}.csv"
informe_caixa(str(date.today()), dades / "vendes.csv", ruta_informe) # (4) 06-02
print(f"Tancament {date.today()}: cataleg desat, copia {copia.name}, "
f"informe {ruta_informe.name}") # (5)
tancament_de_caixa(BASE)Segona execució el mateix dia: les carpetes ja existeixen (exist_ok), el catàleg i la còpia es regeneren amb el mateix nom i l'informe es reescriu — tot estable. Aquest tancament_de_caixa() és, en miniatura, el mòdul 6 sencer funcionant com una sola peça.
Conclusió
El mòdul 6 salda el deute que va deixar obert el tancament del mòdul 5: l'estat de Papyrus ja no mor amb el procés. A 06-01, open() amb els seus modes, el with que garanteix el tancament i l'encoding="utf-8" innegociable van donar a la botiga la seva primera memòria — registre_altes.txt va complir la promesa que fitxa_llibre.py arrossegava des del mòdul 1; el CSV va convertir el catàleg i les vendes en taules que Excel i la distribuïdora entenen, amb DictReader/DictWriter i el round-trip dict[str, Llibre] → cataleg.csv → dict[str, Llibre] pagant el peatge str→float en un únic lloc (06-02); JSON va aportar tipus que sobreviuen al viatge, niament per als socis amb les seves compres, el pont asdict()/Llibre(**dades) i un config.json que treu les constants del codi (06-03); i pathlib va endreçar el basar a dades/, copies/ i informes/, amb rutes ancorades a __file__, glob per trobar, shutil.copy2 per a la còpia nocturna datada i el canvi de nom en lot com a recepta general (06-04). L'Anna apaga l'ordinador i, per primera vegada des del mòdul 1, no perd res. Però fixa't quantes vegades aquest mòdul ha dit "pal·liatiu": Path.exists() abans de llegir, "no editis malament el JSON", "resa perquè el CSV porti números". I si el fitxer no hi és, si la còpia està corrompuda, si float(fila["preu"]) rep "gratis", si la Júlia tecleja un preu impossible? Avui, qualsevol d'aquestes coses atura el programa amb un error a mitja pantalla. El mòdul 7 ensenya a Papyrus a encaixar els cops: excepcions — detectar-les, gestionar-les amb try/except, llançar-les amb intenció i fins i tot dissenyar les teves. Els teus objectes ja saben pensar i recordar; ara aprendran a equivocar-se amb elegància.
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
