El mòdul 5 va acabar amb la confessió més dolorosa de l'Anna: cada nit, en apagar l'ordinador de Papyrus, el catàleg sencer — objectes Llibre amb les seves validacions, els socis, les línies de venda — s'evapora. La RAM és ràpida però oblidadissa: quan el programa acaba, les seves variables moren amb ell. La solució és el disc: escriure les dades en fitxers que sobreviuen a les reinicialitzacions i llegir-los en arrencar. En aquesta lliçó aprendràs open() i els seus modes, la manera correcta de treballar amb fitxers (with), les diferents maneres de llegir i escriure text, i per què encoding="utf-8" t'ha d'acompanyar sempre. De passada saldarem un deute que el curs arrossega des del mòdul 1: fitxa_llibre.py per fi desarà les altes en un fitxer.
Contingut
- Persistència: per què la memòria no és suficient
open(): la porta al disc- Els modes d'obertura:
r,w,a,x with: la manera correcta d'obrir fitxers- Escriure:
write()iwritelines() - Llegir:
read(),readline(),readlines()i iterar línia a línia encoding="utf-8": sempre i explícit- El salt de línia:
\nen escriure,strip()en llegir - Promesa saldada:
fitxa_llibre.pyescriu aregistre_altes.txt - Errors comuns i consells
- Exercicis amb solucions
Persistència: per què la memòria no és suficient
Tot el que has creat fins ara viu a la memòria RAM del procés de Python: existeix mentre el programa s'executa i desapareix quan acaba. Aquesta propietat de les dades de sobreviure entre execucions s'anomena persistència, i la manera més universal d'aconseguir-la és el sistema de fitxers: text pla avui, CSV a 06-02, JSON a 06-03.
flowchart LR
A["RAM<br>cataleg: dict[str, Llibre]<br>ràpida, volàtil"] -- "escriure (aquesta lliçó)" --> B["Disc<br>cataleg.txt, registre_altes.txt<br>lent, persistent"]
B -- "llegir (aquesta lliçó)" --> A
El pla del mòdul és exactament aquest cicle: desar en tancar, carregar en obrir. Papyrus deixarà de llevar-se amnèsica.
open(): la porta al disc
La funció integrada open() connecta el teu programa amb un fitxer del disc i retorna un objecte fitxer amb mètodes per llegir i escriure:
fitxer = open("salutacio.txt", "w", encoding="utf-8") # obre (creant-lo) per escriure
fitxer.write("Benvinguts a Papyrus\n") # escriu una cadena
fitxer.close() # imprescindible! allibera el fitxerEls tres arguments que faràs servir sempre:
- Ruta:
"salutacio.txt"és una ruta relativa — es resol des de la carpeta on executes el programa. Les rutes a fons (absolutes,pathlib) arriben a 06-04; de moment treballa amb fitxers al costat del teu script. - Mode:
"w","r","a"... — la secció següent. encoding: com es tradueixen els caràcters a bytes. Sempre"utf-8", i a la seva secció veuràs per què no és negociable.
close() és crític: fins que no tanques, el que has escrit pot quedar-se en una memòria intermèdia sense arribar al disc, i el fitxer queda bloquejat per a altres programes. I aquí hi ha el problema: si entre open() i close() alguna cosa falla, el close() no s'arriba a executar. Per això gairebé mai escriuràs open()/close() a mà — de seguida arriba with.
Els modes d'obertura: r, w, a, x
El segon argument d'open() decideix què pots fer amb el fitxer i què passa si existeix o no:
| Mode | Significat | Si el fitxer existeix | Si NO existeix | Ús típic a Papyrus |
|---|---|---|---|---|
"r" |
read — llegir (és el mode per defecte) | L'obre per llegir | Error (FileNotFoundError) |
Carregar el catàleg en arrencar |
"w" |
write — escriure | El buida del tot i escriu des de zero | El crea | Abocar el catàleg sencer en tancar |
"a" |
append — afegir | Escriu al final, conservant l'anterior | El crea | Registre d'altes: cada alta, una línia nova |
"x" |
exclusive — crear en exclusiva | Error (FileExistsError) |
El crea | Evitar aixafar un fitxer per accident |
Dues advertències abans de continuar:
"w"és destructiu: tan bon punt executesopen("cataleg.txt", "w", ...), el contingut anterior ja s'ha perdut — encara que després no escriguis res. Triar"w"on tocava"a"és l'error més car d'aquesta lliçó.- Obrir amb
"r"un fitxer que no existeix llançaFileNotFoundError. La gestió elegant d'errors (try/except) és el tema central del mòdul 7; fins llavors farem servir un pal·liatiu explícit — comprovar-ho abans ambPath.exists():
from pathlib import Path # pathlib protagonitzara 06-04; avui nomes li demanem exists()
if Path("cataleg.txt").exists():
... # es segur llegir-lo
else:
print("Encara no hi ha cataleg desat; comencem de zero.")Que quedi dit amb claredat: aquesta comprovació prèvia és un pedaç temporal i té esquerdes (el fitxer podria desaparèixer entre la comprovació i la lectura). L'eina correcta són les excepcions, i les aprendràs al mòdul 7.
També existeixen els modes binaris ("rb", "wb") per a bytes en cru — imatges, o el pickle que esmentarem a 06-03 —, però en aquest mòdul treballem amb text.
with: la manera correcta d'obrir fitxers
La versió professional del primer exemple no porta close():
with open("salutacio.txt", "w", encoding="utf-8") as fitxer:
fitxer.write("Benvinguts a Papyrus\n")
# en sortir del bloc, el fitxer JA esta tancat — passi el que passi a dinswith garanteix el tancament sempre: si el bloc acaba bé, tanca; si a dins salta un error a mig camí, tanca igualment abans de propagar l'error. És l'equivalent a un close() impossible d'oblidar. La variable després d'as (fitxer) és el mateix objecte fitxer d'abans, però amb el cicle de vida gestionat.
with funciona amb fitxers perquè aquests implementen el protocol dels context managers — un mecanisme general de Python que també serveix per a connexions, bloquejos i recursos propis. Aquest protocol complet (i com escriure els teus) s'estudia al mòdul 8; en aquest mòdul en tens prou amb la regla pràctica: tot open() va dins d'un with. Així ho farem a la resta del curs sense excepció.
Escriure: write() i writelines()
write(cadena) escriu exactament la cadena que li passes — ni més ni menys. Dues conseqüències immediates:
- No afegeix salt de línia: si vols línies, escriu tu el
"\n". - Només accepta cadenes: els números cal convertir-los (les f-strings del mòdul 1 són perfectes per a això).
llibres = [("L'Odissea", 12.50, 4), ("Hamlet", 9.95, 6), ("El Quixot", 15.90, 8), ("Faust", 21.00, 0)]
with open("cataleg.txt", "w", encoding="utf-8") as f:
f.write("Cataleg de Papyrus\n") # el \n el poses tu
for titol, preu, estoc in llibres:
f.write(f"{titol} | {preu:.2f} | {estoc}\n") # numeros → text via f-stringContingut resultant de cataleg.txt:
writelines(llista) escriu totes les cadenes d'una llista... i, malgrat el nom, tampoc no afegeix salts de línia. És útil quan ja tens les línies preparades:
linies = [f"{t} | {p:.2f} | {s}\n" for t, p, s in llibres] # comprensio (02-04), \n inclos
with open("cataleg.txt", "w", encoding="utf-8") as f:
f.write("Cataleg de Papyrus\n")
f.writelines(linies)Fixa't en el separador |: l'hem inventat nosaltres, i funciona… fins que un títol contingui una barra vertical. Inventar formats amb separadors casolans és fràgil — exactament el problema que el format CSV i el seu mòdul estàndard resolen a 06-02.
Llegir: read(), readline(), readlines() i iterar línia a línia
Per llegir tens quatre camins; triar bé depèn de la mida del fitxer:
| Mètode | Retorna | Memòria | Quan fer-lo servir |
|---|---|---|---|
f.read() |
TOT el contingut en una sola cadena | Tot el fitxer alhora | Fitxers petits que processes com un bloc |
f.readline() |
La línia següent (amb el seu \n) |
Una línia | Llegir una capçalera i parar |
f.readlines() |
Llista amb totes les línies (amb \n) |
Tot el fitxer alhora | Necessites la llista completa (comptar, indexar) |
for linia in f: |
Una línia per volta | Una línia cada vegada | El mode per defecte: processar línia a línia |
with open("cataleg.txt", "r", encoding="utf-8") as f:
contingut = f.read() # una unica cadena amb els \n inclosos
print(len(contingut))Per a un cataleg.txt de cinc línies qualsevol mètode val. Però imagina l'històric de vendes de deu anys de Papyrus: read() i readlines() el carregarien sencer a la RAM. Iterar sobre el fitxer, en canvi, llegeix una línia, la processa, l'oblida i passa a la següent — memòria constant, tant li fa que el fitxer tingui cinc línies com cinc milions:
with open("cataleg.txt", "r", encoding="utf-8") as f:
next(f) # salta la capcalera "Cataleg de Papyrus"
for linia in f: # l'objecte fitxer es iterable (02-02)
titol, preu, estoc = linia.strip().split(" | ") # 04-05: strip + split
print(f"{titol}: {float(preu):.2f} € — estoc {int(estoc)}")Observa les conversions float(preu) i int(estoc): un fitxer de text només conté text. El 12.50 que vas escriure com a número torna com la cadena "12.50", i recuperar els tipus és responsabilitat teva. Aquest "peatge de conversió" reapareixerà a CSV (06-02) i desapareixerà gairebé del tot amb JSON (06-03).
encoding="utf-8": sempre i explícit
Un fitxer de text és, al capdavall, una seqüència de bytes, i l'encoding és el diccionari que tradueix caràcters ↔ bytes. Amb les lletres ASCII (a–z) tots els diccionaris coincideixen; el problema són els accents, la ç, l'€, la ela geminada (l·l)… Si no passes encoding, Python fa servir el de la plataforma: UTF-8 a macOS/Linux, però històricament cp1252 a Windows. El mateix programa que al teu portàtil escriu García pot llegir GarcÃa (o llançar UnicodeDecodeError) a l'ordinador de la botiga.
# MALAMENT: encoding implicit — depen de la maquina que ho executa
with open("cataleg.txt", "w") as f:
f.write("Antologia de García Lorca — 14 €\n")
# BE: explicit — el fitxer es identic a Windows, macOS i Linux
with open("cataleg.txt", "w", encoding="utf-8") as f:
f.write("Antologia de García Lorca — 14 €\n")Regla del curs, sense excepcions: tot open() de text porta encoding="utf-8", en escriure i en llegir (amb el mateix diccionari en totes dues direccions, és clar). UTF-8 és l'estàndard de facto de la web i de gairebé tot format modern. Una llibreria amb accents i eles geminades a la meitat de les seves fitxes no es pot permetre cap altra cosa.
El salt de línia: \n en escriure, strip() en llegir
El caràcter \n amb què separes les línies en escriure viatja dins de cada línia en llegir: readline(), readlines() i la iteració el conserven al final de la cadena. Si l'oblides, apareixen comparacions que fallen i prints amb línies en blanc fantasma:
with open("cataleg.txt", "r", encoding="utf-8") as f:
primera = f.readline()
print(repr(primera)) # 'Cataleg de Papyrus\n' ← aqui el tens
print(primera == "Cataleg de Papyrus") # False (per culpa del \n)
print(primera.strip() == "Cataleg de Papyrus") # TrueL'antídot és un vell conegut: strip() (04-05) en processar cada línia llegida — elimina el \n final i, de regal, els espais accidentals. El patró canònic de lectura queda així: for linia in f: + linia.strip() com a primer pas. (Python a més normalitza automàticament els salts de Windows \r\n a \n en mode text, així que strip() també et cobreix aquest cas.)
Promesa saldada: fitxa_llibre.py escriu a registre_altes.txt
Al mòdul 1, fitxa_llibre.py demanava les dades d'un llibre amb input() i mostrava la fitxa amb print() — i vam prometre que algun dia aquesta fitxa no moriria en tancar la finestra. Aquest dia és avui. La versió definitiva dona d'alta el llibre i en deixa constància al disc, fent servir el mode "a" perquè cada execució afegeixi una línia sense esborrar les anteriors:
"""fitxa_llibre.py — alta de llibres amb registre persistent (promesa del modul 1)."""
from dataclasses import dataclass
from datetime import date # biblioteca estandard, 03-05
NOM_BOTIGA = "Papyrus"
REGISTRE = "registre_altes.txt"
@dataclass
class Llibre: # la dataclass canonica de 05-06 (versio minima)
titol: str
preu: float
estoc: int = 0
def __post_init__(self):
self.titol = self.titol.strip()
if not self.titol:
raise ValueError("El titol no pot estar buit")
if self.preu < 0:
raise ValueError(f"Preu negatiu: {self.preu}")
def registrar_alta(llibre):
"""Afegeix una linia al registre d'altes. Mode 'a': mai no esborra l'anterior."""
with open(REGISTRE, "a", encoding="utf-8") as f:
f.write(f"{date.today()} | ALTA | {llibre.titol} | {llibre.preu:.2f} | {llibre.estoc}\n")
if __name__ == "__main__": # 03-04: executable i importable
titol = input("Titol: ")
preu = float(input("Preu (€): "))
estoc = int(input("Estoc: "))
llibre = Llibre(titol, preu, estoc)
registrar_alta(llibre)
print(f"{llibre.titol} donat d'alta a {NOM_BOTIGA} i registrat a {REGISTRE}.")Després de donar d'alta dos llibres en dies diferents, registre_altes.txt acumula la història:
Cada peça ve d'un mòdul anterior: input()/f-strings (M1), la dataclass validant en néixer (05-06), date.today() (03-05), i la novetat — with + "a" + encoding="utf-8" — hi posa la persistència. L'Anna ja té una cosa que l'apagada nocturna no pot esborrar.
Errors Comuns i Consells
- Obrir amb
"w"quan volies"a": el fitxer es buida en l'instant de l'open(). Si és un registre històric (altes, vendes), és"a";"w"és per a abocaments complets que es regeneren sencers. - Oblidar
encoding="utf-8": funciona a la teva màquina i rebenta (o corromp els accents) en una altra. Explícit sempre, en lectura i en escriptura. - Esperar que
write()owritelines()afegeixin\n: no ho fan. Tu poses els salts; el nomwritelinesenganya. - Oblidar
strip()en llegir: el\ninvisible fa fallar comparacions isplit(). Primer reflex amb cada línia llegida:linia.strip(). - Llegir amb
read()/readlines()fitxers potencialment grans: ho carreguen tot a la RAM. El mode per defecte és iterar:for linia in f:. - Escriure fora del
with: després del bloc, el fitxer està tancat;f.write(...)llançaValueError: I/O operation on closed file. Tota la feina ambf, dins del bloc. - Oblidar convertir tipus en llegir: del fitxer tot torna com a
str."12.50" + 1és unTypeError; converteix ambfloat()/int()just després de fersplit(). - Consell: els fitxers de text s'inspeccionen gratis — obre'ls amb qualsevol editor per verificar què va escriure el teu programa. Aquesta transparència és un gran avantatge respecte dels formats binaris; aprofita-la mentre depures.
Exercicis
Exercici 1: inventari nocturn
Escriu desar_inventari(cataleg, ruta) que rebi el catàleg canònic dict[str, Llibre] (05-06) i escrigui a ruta una línia per llibre amb el format titol | preu | estoc (preu amb 2 decimals), en mode "w" — és l'abocament complet del tancament de caixa, es regenera sencer cada nit. Prova-ho amb els quatre llibres canònics i obre el fitxer amb el teu editor per comprovar-ho.
Exercici 2: recarregar el catàleg en obrir la botiga
Escriu carregar_inventari(ruta) que faci el viatge invers: llegeixi el fitxer de l'exercici 1 línia a línia (iterant, no amb read()), reconstrueixi cada Llibre amb els tipus correctes i retorni el dict[str, Llibre] amb claus normalitzades (normalitzar_titol, 04-05). Si el fitxer no existeix, retorna {} fent servir el pal·liatiu Path.exists() — i deixa-hi un comentari recordant que el mòdul 7 portarà la solució de veritat. Comprova que carregar_inventari("inventari.txt")["hamlet"].preu_final(soci=True) retorna 9.83.
Exercici 3: dietari d'obertura i tancament
Escriu anotar(esdeveniment) que afegeixi a diari.txt (mode "a") una línia data | esdeveniment, amb la data de date.today(). Crida anotar("Obertura de Papyrus") i anotar("Tancament de caixa"), executa l'script dues vegades i comprova que el dietari conserva les quatre línies. Després escriu comptar_esdeveniments(ruta) que retorni un Counter (04-06) d'esdeveniments llegint el dietari línia a línia.
Solucions
Solució 1:
def desar_inventari(cataleg, ruta):
with open(ruta, "w", encoding="utf-8") as f: # "w": abocament complet
for llibre in cataleg.values():
f.write(f"{llibre.titol} | {llibre.preu:.2f} | {llibre.estoc}\n")
desar_inventari(cataleg, "inventari.txt")Iterem sobre values() (04-03) perquè la clau normalitzada és reconstruïble; el títol "bonic" viu a l'objecte.
Solució 2:
from pathlib import Path
def carregar_inventari(ruta):
if not Path(ruta).exists(): # pedac fins al try/except del modul 7
return {}
cataleg = {}
with open(ruta, "r", encoding="utf-8") as f:
for linia in f: # linia a linia: memoria constant
titol, preu, estoc = linia.strip().split(" | ")
llibre = Llibre(titol, float(preu), int(estoc)) # str → tipus reals
cataleg[normalitzar_titol(llibre.titol)] = llibre
return cataleg
cataleg = carregar_inventari("inventari.txt")
print(cataleg["hamlet"].preu_final(soci=True)) # 9.83 — la xifra canonicaEl round-trip complet: els objectes que vas desar ahir a la nit tornen a la vida aquest matí, validats per __post_init__ en reconstruir-se.
Solució 3:
from datetime import date
from collections import Counter
def anotar(esdeveniment):
with open("diari.txt", "a", encoding="utf-8") as f:
f.write(f"{date.today()} | {esdeveniment}\n")
def comptar_esdeveniments(ruta):
esdeveniments = Counter()
with open(ruta, "r", encoding="utf-8") as f:
for linia in f:
_, esdeveniment = linia.strip().split(" | ")
esdeveniments[esdeveniment] += 1
return esdeveniments
anotar("Obertura de Papyrus")
anotar("Tancament de caixa")
print(comptar_esdeveniments("diari.txt"))
# despres de dues execucions: Counter({'Obertura de Papyrus': 2, 'Tancament de caixa': 2})El mode "a" acumula entre execucions — això és exactament la persistència que Papyrus necessitava.
Conclusió
Papyrus ja té memòria a llarg termini: open() amb el mode adequat (r per llegir, w per abocar, a per acumular, x per protegir), sempre dins d'un with que garanteix el tancament, sempre amb encoding="utf-8" perquè els accents i les ces trencades sobrevisquin al viatge, escrivint \n a consciència i netejant amb strip() en tornar. La promesa del mòdul 1 està saldada: fitxa_llibre.py deixa empremta a registre_altes.txt i l'inventari ressuscita cada matí. Però el nostre format titol | preu | estoc és un dialecte casolà: l'hem inventat nosaltres, ningú més no l'entén, i es trenca el dia que un títol contingui una barra vertical. Existeix un format de text tabular que Excel, Google Sheets i mig món parlen de naixement, amb un mòdul de la biblioteca estàndard que et resol separadors, cometes i casos retorçats: CSV. A la propera lliçó, el catàleg i les vendes de Papyrus s'hi muden.
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
