Lettura e scrittura di file
Leggere e scrivere con `with open`, cosa distingue r/w/a/x, l'a capo alla fine di ogni riga, perché `encoding` viene scritto sempre, e pathlib per i lavori piccoli.
Come vuoi imparare?
Il problema che stiamo risolvendo
Tutto ciò che abbiamo costruito in ventidue capitoli è sparito nel momento in cui il programma è finito.
basket = []
basket.append("pen")
print(basket)['pen']Eseguilo di nuovo e il cestino è di nuovo vuoto. Liste, dizionari, funzioni — tutto vive in memoria, e la memoria dura esattamente quanto dura il programma.
Il capitolo precedente parlava di condividere codice tra programmi. Questo parla di dati — come un'esecuzione lascia il proprio lavoro alla successiva, o a qualcun altro.
with open("notes.txt", "w", encoding="utf-8") as fh:
fh.write("first line\n")
fh.write("second line\n")
with open("notes.txt", "r", encoding="utf-8") as fh:
print(fh.read(), end="")first line
second lineIl programma è finito, e notes.txt è ancora lì.
Alla fine di questo capitolo sarai in grado di
- Leggere e scrivere file con
with open(...), e dire perchéwith - Dire cosa distingue
"r","w","a"e"x"— soprattutto cosa distrugge"w" - Leggere un file una riga alla volta, e gestire il
\nalla fine di ciascuna riga - Dire perché
encoding="utf-8"viene scritto sempre - Usare
pathlib.Pathper i lavori piccoli - Leggere un
FileNotFoundErrore unUnicodeDecodeError
Prerequisiti: Moduli e import.
Perché with
Aprire un file prende in prestito qualcosa dal sistema operativo, e un prestito va ripagato. Senza chiudere, ciò che hai scritto potrebbe non arrivare mai sul disco, e abbastanza file non chiusi fermeranno il programma.
Un blocco with fa quel ripagamento da solo — il file viene chiuso quando il blocco finisce, anche se qualcosa dentro ha sollevato un'eccezione.
with open("notes.txt", "r", encoding="utf-8") as fh:
text = fh.read()
print(fh.closed)
print(fh.read())True
ValueError: I/O operation on closed file.(Prima della riga dell'errore Python stampa anche alcune righe di traceback; in questo capitolo viene mostrata solo l'ultima.)
Il nome fh esiste ancora dopo il blocco — lo scope funziona esattamente come prima — ma il file è chiuso. In questo corso open() viene scritto sempre con with.
Leggere
L'intero contenuto in una volta sola si ottiene da .read():
with open("notes.txt", "r", encoding="utf-8") as fh:
text = fh.read()
print(repr(text))
print(len(text))'first line\nsecond line\n'
23repr viene usato apposta, perché un semplice print nasconde gli a capo. Un file è un unico lungo pezzo di testo, con i suoi a capo che ci stanno dentro come \n — ricordarlo è metà di questo capitolo.
Una riga alla volta
.read() è una scelta pessima per un file grande, perché solleva tutto in memoria. Ciclare sul file stesso fa leggere a Python una riga alla volta:
with open("orders.txt", "r", encoding="utf-8") as fh:
for line in fh:
print(repr(line))'pen,15.0,3\n'
'bag,850.0,1\n'
'ink,120.0,2\n'Ogni riga finisce con \n. Questa è la confusione più comune di tutte — line == "pen" non corrisponde, "pen\n" e "pen" sono chiavi di dizionario diverse, e il motivo è invisibile. (int() e float() ignorano da soli spazi e \n attorno al numero, quindi int("3\n") dà davvero 3 — ma una stringa viene tenuta esattamente com'è, quindi il \n resta nel confronto e nell'output stampato.)
Quindi la prima mossa è quasi sempre .strip():
with open("orders.txt", "r", encoding="utf-8") as fh:
for line in fh:
name, price, qty = line.strip().split(",")
print(f"{name:<5} {float(price) * int(qty):>8.2f}")pen 45.00
bag 850.00
ink 240.00.strip() poi .split(",") poi float() e int() — quella sequenza tornerà ancora e ancora quando si leggono file. Tutto ciò che esce da un file è testo, mai numeri; la conversione del capitolo 7 serve anche qui.
Quando servono tutte le righe in una volta sola, ci sono due modi:
with open("orders.txt", "r", encoding="utf-8") as fh:
lines = fh.read().splitlines()
print(lines)['pen,15.0,3', 'bag,850.0,1', 'ink,120.0,2'].splitlines() elimina gli a capo. C'è anche .readlines(), ma tiene il \n — è lì che la maggior parte della gente inciampa.
Scrivere, e una lettera pericolosa
with open("notes.txt", "w", encoding="utf-8") as fh:
fh.write("new\n")Il file conteneva prima old content. Ora:
new"w" svuota il file — nel momento in cui lo apri, prima che venga scritto qualcosa. Nessun avvertimento e nessun modo per tornare indietro.
Per tenere ciò che c'è e aggiungere in coda, "a":
with open("notes.txt", "a", encoding="utf-8") as fh:
fh.write("new\n")old content
newE per rifiutarsi di scrivere del tutto se il file esiste già, "x":
FileExistsError: [Errno 17] File exists: 'notes.txt'Le quattro modalità in una riga: "r" legge, "w" cancella e scrive, "a" aggiunge in coda, "x" solo crea.
Fai una pausa prima di scrivere"w". La gente perde lavoro vero a causa di questa singola lettera. Se il file che stai aprendo non è stato creato dal tuo programma, rifletti prima di scrivere"w".
Il \n spetta a te
print ti spezza la riga. write no.
rows = ["pen", "bag"]
with open("out.txt", "w", encoding="utf-8") as fh:
for row in rows:
fh.write(row)'penbag'Quindi il modo più pulito per scrivere una lista di righe è:
rows = ["pen", "bag"]
with open("out.txt", "w", encoding="utf-8") as fh:
fh.write("\n".join(rows) + "\n")'pen\nbag\n'"\n".join(rows) mette gli a capo tra le righe, e il + "\n" finale chiude il file con un a capo — che è la regola abituale per i file di testo, e ciò che molti strumenti si aspettano.
encoding="utf-8" — sempre
Senza encoding, Python usa il predefinito del sistema operativo, che cambia da macchina a macchina. Il risultato è un programma che funziona dove sei tu e si rompe dove c'è qualcun altro.
Leggere con quello sbagliato dà:
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xff in position 3: invalid start byteQuindi scrivi encoding="utf-8" ogni volta che usi open() — sia per leggere sia per scrivere. L'unica eccezione è la modalità binaria ("rb", "wb"), dove la domanda non si pone perché non c'è encoding.
pathlib — un percorso breve per i lavori piccoli
Per un file piccolo, l'intero blocco with può sembrare eccessivo.
from pathlib import Path
path = Path("orders.txt")
print(path.exists())
print(len(path.read_text(encoding="utf-8").splitlines()))
out = Path("report.txt")
out.write_text("done\n", encoding="utf-8")
print(out.read_text(encoding="utf-8"), end="")True
3
doneread_text e write_text aprono e chiudono il file internamente, quindi non serve with. Path fa anche altro — .exists(), .name, .suffix, .parent — e i percorsi delle cartelle si uniscono con /, come in Path("data") / "orders.txt".
Quale quando? Path quando tutto il file serve in una volta sola, with open(...) quando si legge riga per riga. Tieni a mente una cosa: write_text si comporta sempre come "w" — se il file esiste già, il suo vecchio contenuto viene cancellato senza alcun avvertimento. Non ha modo di aggiungere in coda; per quello serve with open(..., "a").
Un esempio completo
orders.txt:
pen,15.0,3
bag,850.0,1
ink,120.0,2main.py:
"""Read an order file, total it, and write a report beside it."""
from pathlib import Path
TAX_RATE = 0.15
def read_order(path):
"""Each line is name,price,quantity. Blank lines are skipped."""
lines = []
for raw in path.read_text(encoding="utf-8").splitlines():
if not raw.strip():
continue
name, price, quantity = raw.split(",")
lines.append((name, float(price), int(quantity)))
return lines
def report(lines):
rows = []
total = 0.0
for name, price, quantity in lines:
amount = round(price * quantity * (1 + TAX_RATE), 2)
total += amount
rows.append(f"{name:<6} {quantity:>3} {amount:>9.2f}")
rows.append("-" * 20)
rows.append(f"{'total':<6} {'':>3} {total:>9.2f}")
return rows
def main():
order = read_order(Path("orders.txt"))
rows = report(order)
Path("report.txt").write_text("\n".join(rows) + "\n", encoding="utf-8")
print(Path("report.txt").read_text(encoding="utf-8"), end="")
print()
print("lines read:", len(order))
if __name__ == "__main__":
main()pen 3 51.75
bag 1 977.50
ink 2 276.00
--------------------
total 1305.25
lines read: 3Quattro cose che val la pena guardare.
Tre funzioni con tre lavori separati. read_order legge dal disco, report calcola soltanto, e main scrive. A report non viene passato alcun file — gli viene passata una lista e restituisce una lista. Quindi testarla non richiede alcun file, e al capitolo 28 sarà esattamente quello che aiuterà.
Le righe vuote sono gestite in anticipo. Un singolo \n alla fine del file non produce una riga vuota — .splitlines() lo elimina. Ma in un file modificato a mano, una vera riga vuota (due \n di fila) nel mezzo o alla fine è del tutto normale, e senza gestirla raw.split(",") non otterrebbe tre cose e solleverebbe un ValueError. if not raw.strip(): continue risolve il tutto in una riga.
float(price) e int(quantity) non sono opzionali, perché tutto ciò che esce da un file arriva come testo. Se li dimentichi, price * quantity ripeterebbe in silenzio la stringa invece — il peggior tipo di bug, quello che non si rompe.
Il report va in un file separato. orders.txt non viene mai aperto con "w" — il file di input resta intatto. È un'abitudine che vale la pena formare: non scrivere su ciò che stai leggendo.
Quando si rompe
FileNotFoundError: [Errno 2] No such file or directory: 'orders.txt' Il file non c'è, o stai eseguendo il programma da una cartella diversa. Il percorso viene ricavato da dove lo hai eseguito, non dalla cartella dello script. Stampa Path("orders.txt").resolve() per vedere dove sta cercando.
UnicodeDecodeError: 'utf-8' codec can't decode byte ... Il file è stato scritto con un'altra codifica, oppure non è un file di testo. Correggi l'encoding, o aprilo con "rb" se è binario.
ValueError: I/O operation on closed file. Il file è stato usato fuori dal suo blocco with. Leggi ciò che ti serve dentro il blocco.
ValueError: not enough values to unpack Qualche riga non ha le virgole che ti aspettavi — spesso una riga vuota. Salta le righe vuote, e in caso di dubbio print(repr(raw)).
Ho scritto il file ma è vuoto Viene letto prima che il blocco with sia finito. Le scritture arrivano sul disco quando il file viene chiuso.
Tutte le mie righe sono finite insieme in una sola write non spezza le righe. Il "\n" spetta a te.
Tutto nel mio file è sparito È stato aperto con "w". Usa "r" per leggere e "a" per aggiungere in coda.
Passo 4 di 6 — Prevedi
Verifica cosa hai capito
repr viene usato, quindi nulla è nascosto. Cosa viene stampato?
# orders.txt
# pen,15.0,3
# bag,850.0,1
# ink,120.0,2
with open("orders.txt", "r", encoding="utf-8") as fh:
for line in fh:
print(repr(line))- A'pen,15.0,3' 'bag,850.0,1' 'ink,120.0,2'
- B'pen,15.0,3\nbag,850.0,1\nink,120.0,2\n'
- C'pen,15.0,3\n' 'bag,850.0,1\n' 'ink,120.0,2\n'
- D['pen,15.0,3', 'bag,850.0,1', 'ink,120.0,2']
Il file è aperto con "w" ma non viene scritto nulla. Cosa viene stampato?
# notes.txt already contains: important work
with open("notes.txt", "w", encoding="utf-8") as fh:
print("opened")
with open("notes.txt", "r", encoding="utf-8") as fh:
print(repr(fh.read()))- Aopened 'opened\n'
- BUn `FileExistsError`
- Copened 'important work\n'
- Dopened ''
Due cose vengono scritte separatamente. Cosa c'è nel file?
rows = ["pen", "bag"]
with open("out.txt", "w", encoding="utf-8") as fh:
for row in rows:
fh.write(row)
with open("out.txt", "r", encoding="utf-8") as fh:
print(repr(fh.read()))- A'pen\nbag\n'
- B'pen\nbag'
- C'pen bag'
- D'penbag'
Per rispondere serve un account
Accedi per verificare le tue risposte
Le domande sono qui sopra, e ragionarci a mente è la parte che conta. Accedi per vedere le risposte, le spiegazioni e i suggerimenti a tre livelli.
Tocca a te
Crea un file chiamato people.txt con un nome e un numero su ogni riga, separati da una virgola — almeno quattro righe, e lascia apposta una riga vuota nel mezzo.
Poi scrivi summary.py con tre funzioni: una che legge il file in una lista di coppie, una che trasforma quella lista in righe di testo (chi ha il numero più grande, e il totale), e main che scrive il tutto in summary.txt. Non dimenticare if __name__ == "__main__":.
Poi cinque esperimenti:
- Rimuovi lo
.strip(), poi stampa l'ultima parte di ogni riga conrepr().int()solleva un errore? (No —int("10\n")dà10.) Allora dove si nasconde il\n, e cosa succede sulla riga vuota? - Cancella il numero da una riga in modo che la virgola ci sia ma il numero no. Leggi il messaggio.
- Cambia
"w"in"a"inmain, poi esegui il programma tre volte. Cosa è successo asummary.txt? - Prova ad aprire
people.txtcon"w"... non farlo. Fai una copia e fallo invece a quella, poi guarda cosa resta. - Stampa
Path("people.txt").resolve(), poi esegui il programma da una cartella diversa. Il percorso cambia?
Quello ultimo spiega un errore che quasi tutti commettono almeno una volta: il programma ha funzionato, poi è stato eseguito da qualche altra parte e ha sollevato un FileNotFoundError.
Passo 6 di 6
Sfida — il quiz del capitolo
Dieci domande dalla più facile alla più difficile. Le ultime sono difficili di proposito.
Accedi per fare il quiz