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

  1. Rutes: la dada que sembla una cadena però no ho és
  2. pathlib.Path: rutes orientades a objectes
  3. Rutes relatives vs absolutes i Path.cwd()
  4. Anatomia d'una ruta: name, stem, suffix, parent
  5. Preguntar i crear: exists(), is_file(), is_dir(), mkdir()
  6. Cercar fitxers amb glob()
  7. os i os.path: el llegat (taula d'equivalències)
  8. shutil: copiar, moure i esborrar arbres
  9. L'estructura definitiva del projecte papyrus/
  10. Còpia de seguretat nocturna amb data
  11. Canvi de nom en lot: els informes diaris
  12. Errors comuns i consells
  13. 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 amb Path.cwd().
print(Path.cwd())                        # C:\botigues\papyrus  (des d'on has executat python)
print((Path("dades") / "cataleg.json").resolve())   # la versio absoluta de la relativa

La 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'executi

Aquest 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 canonica

Els 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/2026 d'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 COMPLET
  • copy2() copia el fitxer conservant metadades com la data de modificació (la seva germana copy() 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ó. Un rmtree sobre la ruta equivocada és irrecuperable. Regla d'or: mai sobre una ruta construïda amb dades de l'usuari, i sempre després d'un is_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 nomdatetime (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.json

Disecciona 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.csv

El 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 / de Path existeix 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: sense parents=True falla si falta un tram; sense exist_ok=True falla 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 amb sorted().
  • rename() cap a un nom que ja existeix: en alguns sistemes sobreescriu silenciosament el destí. Comprova exists() abans, com a normalitzar_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 si move() a una carpeta paperera/ 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 amb Path i l'operador /.
  • Consell: posa als fitxers datats noms sempre en ISO (aaaa-mm-dd). Ordre alfabètic = ordre cronològic, els glob per 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 recents

L'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.csvdict[str, Llibre] pagant el peatge strfloat 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

Mòdul 2: Estructures de Control

Mòdul 3: Funcions i Mòduls

Mòdul 4: Estructures de Dades

Mòdul 5: Programació Orientada a Objectes

Mòdul 6: Gestió de Fitxers

Mòdul 7: Gestió d'Errors i Excepcions

Mòdul 8: Temes Avançats

Mòdul 9: Proves i Depuració

Mòdul 10: Desenvolupament Web amb Python

Mòdul 11: Ciència de Dades amb Python

Mòdul 12: Projecte Final

© Copyright 2026. Tots els drets reservats