Fino a questo punto i nostri programmi hanno lavorato soprattutto con dati presenti in memoria.
Possiamo creare variabili:
nome = "Anna"
eta = 25
e strutture dati:
studenti = [
{"nome": "Anna", "voto": 8},
{"nome": "Marco", "voto": 7},
]
Ma cosa succede quando il programma termina?
I dati presenti nelle variabili vengono persi.
Per conservare informazioni tra un'esecuzione e l'altra abbiamo bisogno della persistenza.
Uno dei sistemi più semplici per ottenere persistenza è utilizzare i file.
In questo capitolo impareremo a:
with;pathlib;Prerequisiti: funzioni, strutture dati, comprehension, classi ed eccezioni.
Il termine I/O significa:
Input / Output
L'input è un dato che entra nel programma.
L'output è un dato che il programma produce verso l'esterno.
Abbiamo già incontrato l'input da tastiera:
nome = input("Come ti chiami? ")
e l'output:
print(f"Ciao {nome}")
I file rappresentano un'altra forma di input/output.
Possiamo quindi avere:
Tastiera ─────► programma ─────► schermo
│
│
▼
file
Un programma può:
Supponiamo di avere:
studenti = [
{"nome": "Anna", "voto": 8},
{"nome": "Marco", "voto": 7},
]
Finché il programma è in esecuzione, i dati sono disponibili.
Quando il programma termina, però, la lista viene persa.
Possiamo invece salvarla in un file.
Esempi di dati che normalmente vengono salvati su file:
I file sono quindi uno dei primi strumenti attraverso cui un programma può interagire con dati persistenti.
open()Per lavorare con un file possiamo utilizzare:
open()
La forma fondamentale è:
file = open(nome_file, modalita)
Per esempio:
file = open("dati.txt", "r")
Abbiamo aperto dati.txt in modalità lettura.
Dopo averlo utilizzato, dovremmo chiuderlo:
file.close()
Esempio completo:
file = open("dati.txt", "r")
contenuto = file.read()
file.close()
print(contenuto)
Questo approccio funziona, ma non è quello che utilizzeremo normalmente.
La soluzione preferibile è il context manager con with.
Le modalità più importanti sono:
Modalità Significato
"r" lettura
"w" scrittura, sostituisce il contenuto
"a" scrittura in coda
"x" crea un nuovo file, fallisce se esiste
"b" modalità binaria
"t" modalità testo, normalmente implicita
"+" lettura e scrittura
Possiamo combinare i modificatori.
Per esempio:
"rb"
significa lettura binaria.
"wb"
significa scrittura binaria.
"r+"
significa lettura e scrittura sullo stesso file.
Se non specifichiamo la modalità:
open("dati.txt")
Python utilizza:
"r"
"r" --- letturawith open("dati.txt", "r") as file:
contenuto = file.read()
print(contenuto)
Il file deve esistere.
Se non esiste:
FileNotFoundError
encodingPer i file di testo è buona pratica specificare esplicitamente l'encoding:
with open("dati.txt", "r", encoding="utf-8") as file:
contenuto = file.read()
UTF-8 è uno standard molto diffuso e permette di rappresentare
correttamente caratteri come:
à è ì ò ù
€
e moltissimi altri caratteri Unicode.
"w" --- scritturawith open("dati.txt", "w", encoding="utf-8") as file:
file.write("Ciao Python!\n")
Se il file non esiste, viene creato.
Se esiste già, il contenuto viene sostituito.
Questo è un punto fondamentale.
with open("dati.txt", "w", encoding="utf-8") as file:
file.write("Nuovo contenuto")
non significa "aggiungi".
Significa:
apri il file per scrivere e sostituisci il contenuto precedente.
Questo codice:
with open("dati.txt", "w", encoding="utf-8") as file:
file.write("A")
seguito da:
with open("dati.txt", "w", encoding="utf-8") as file:
file.write("B")
lascia nel file:
B
non:
AB
"a" --- appendSe vogliamo aggiungere dati alla fine:
with open("dati.txt", "a", encoding="utf-8") as file:
file.write("Nuova riga\n")
Se il file non esiste, viene creato.
Se esiste, il nuovo contenuto viene aggiunto in coda.
Esempio:
with open("log.txt", "a", encoding="utf-8") as file:
file.write("Applicazione avviata\n")
È una modalità particolarmente utile per:
Scrivi un programma che:
saluto.txt;"Ciao Python!";"Sto imparando il File I/O.".Poi leggi e stampa il contenuto.
"x" --- creazione esclusivaLa modalità:
"x"
crea un nuovo file.
Se il file esiste già, viene generata un'eccezione:
FileExistsError
Esempio:
with open("nuovo.txt", "x", encoding="utf-8") as file:
file.write("File appena creato")
È utile quando vogliamo essere certi di non sovrascrivere accidentalmente un file esistente.
withLa forma consigliata è:
with open(...) as file:
...
Il with garantisce che il file venga chiuso correttamente quando si
esce dal blocco.
Per esempio:
with open("dati.txt", "r", encoding="utf-8") as file:
contenuto = file.read()
Non dobbiamo scrivere:
file.close()
Supponiamo:
file = open("dati.txt", "r")
contenuto = file.read()
elabora(contenuto)
file.close()
Se elabora() genera un'eccezione, file.close() potrebbe non essere
raggiunto.
Con:
with open("dati.txt", "r", encoding="utf-8") as file:
contenuto = file.read()
elabora(contenuto)
la risorsa viene gestita correttamente all'uscita dal contesto.
Questo collega direttamente il File I/O al capitolo precedente sulle eccezioni e i context manager.
read()Il metodo:
read()
restituisce il contenuto disponibile del file come stringa.
with open("dati.txt", "r", encoding="utf-8") as file:
contenuto = file.read()
print(contenuto)
Se il file contiene:
Prima riga
Seconda riga
Terza riga
contenuto sarà una stringa che contiene anche i caratteri di fine
riga.
read(n)Possiamo specificare quanti caratteri leggere:
with open("dati.txt", "r", encoding="utf-8") as file:
parte = file.read(10)
Vengono letti al massimo 10 caratteri dalla posizione corrente.
Quando leggiamo un file, Python mantiene una posizione corrente, chiamata spesso file position o puntatore.
Esempio:
with open("dati.txt", "r", encoding="utf-8") as file:
print(file.read(5))
print(file.read(5))
La seconda chiamata continua dalla posizione raggiunta dalla prima.
Possiamo conoscere la posizione con:
file.tell()
Esempio:
with open("dati.txt", "r", encoding="utf-8") as file:
print(file.tell())
file.read(5)
print(file.tell())
seek()Possiamo spostare la posizione corrente con:
file.seek(posizione)
Esempio:
with open("dati.txt", "r", encoding="utf-8") as file:
print(file.read(5))
file.seek(0)
print(file.read(5))
Il secondo read(5) riparte dall'inizio.
tell() e seek()tell() → dove mi trovo?
seek() → spostami qui
Sono particolarmente utili quando lavoriamo direttamente con la posizione all'interno di un file.
readline()Il metodo:
readline()
legge una riga alla volta.
with open("dati.txt", "r", encoding="utf-8") as file:
riga = file.readline()
while riga != "":
print(riga, end="")
riga = file.readline()
Quando si raggiunge la fine del file, readline() restituisce:
""
end=""?La riga letta contiene normalmente già \n.
Se scriviamo:
print(riga)
possiamo ottenere una riga vuota tra le righe visualizzate.
Con:
print(riga, end="")
manteniamo il newline già presente nel file.
forIn Python esiste una soluzione molto più naturale:
with open("dati.txt", "r", encoding="utf-8") as file:
for riga in file:
print(riga, end="")
Questa è generalmente la soluzione preferibile quando vogliamo processare un file riga per riga.
Il file è un oggetto iterabile.
Non dobbiamo gestire manualmente:
while riga != "":
Più verboso:
riga = file.readline()
while riga != "":
...
riga = file.readline()
Più Pythonico:
for riga in file:
...
Scrivi un programma che legga dati.txt riga per riga e stampi ogni
riga preceduta dal numero progressivo:
1: Prima riga
2: Seconda riga
3: Terza riga
readlines()Il metodo:
readlines()
restituisce una lista contenente le righe.
with open("dati.txt", "r", encoding="utf-8") as file:
righe = file.readlines()
Se il file contiene:
Anna
Marco
Luca
potremmo avere:
[
"Anna\n",
"Marco\n",
"Luca\n"
]
readlines() carica tutte le righe in memoria.
Per file piccoli può essere comodo.
Per file molto grandi è spesso preferibile:
for riga in file:
...
che permette di elaborare progressivamente il contenuto.
strip(), rstrip() e splitlines()Quando leggiamo righe da un file è comune incontrare:
"Anna\n"
Possiamo rimuovere il carattere di fine riga con:
riga.rstrip("\n")
Oppure:
riga.strip()
ma attenzione: strip() rimuove anche spazi e altri caratteri bianchi
all'inizio e alla fine.
rstrip()riga.rstrip()
rimuove gli spazi bianchi finali.
splitlines()Se abbiamo già letto tutto il contenuto:
contenuto = file.read()
possiamo ottenere le righe con:
righe = contenuto.splitlines()
Esempio:
with open("dati.txt", "r", encoding="utf-8") as file:
righe = file.read().splitlines()
Questo evita di mantenere il \n nelle stringhe.
write()Il metodo:
write()
scrive una stringa.
with open("output.txt", "w", encoding="utf-8") as file:
file.write("Prima riga\n")
file.write("Seconda riga\n")
write() non aggiunge automaticamente \nQuesta istruzione:
file.write("Ciao")
scrive esattamente:
Ciao
Per andare a capo dobbiamo scrivere:
file.write("Ciao\n")
write() restituisce normalmente il numero di caratteri scritti.
with open("output.txt", "w", encoding="utf-8") as file:
n = file.write("Ciao")
print(n)
Possiamo utilizzare più chiamate:
with open("nomi.txt", "w", encoding="utf-8") as file:
file.write("Anna\n")
file.write("Marco\n")
file.write("Luca\n")
Oppure preparare una stringa:
nomi = "Anna\nMarco\nLuca\n"
with open("nomi.txt", "w", encoding="utf-8") as file:
file.write(nomi)
Oppure utilizzare writelines().
writelines()Il metodo:
writelines()
scrive una sequenza di stringhe.
righe = [
"Anna\n",
"Marco\n",
"Luca\n",
]
with open("nomi.txt", "w", encoding="utf-8") as file:
file.writelines(righe)
writelines() non aggiunge automaticamente il carattere \n.
Quindi:
righe = ["Anna", "Marco", "Luca"]
produce:
AnnaMarcoLuca
Per ottenere righe separate dobbiamo includere \n:
righe = ["Anna\n", "Marco\n", "Luca\n"]
Oppure:
with open("nomi.txt", "w", encoding="utf-8") as file:
file.writelines(f"{nome}\n" for nome in nomi)
Supponiamo di voler generare numeri casuali:
import random
n = int(input("Quanti numeri? "))
with open("numeri.txt", "w", encoding="utf-8") as file:
for _ in range(n):
numero = random.randint(0, 100)
file.write(f"{numero}\n")
Il file conterrà un numero per riga.
Per esempio:
12
84
3
56
91
Questo esempio combina:
I dati letti da un file di testo sono stringhe.
Se il file contiene:
10
20
30
questo:
with open("numeri.txt", "r", encoding="utf-8") as file:
for riga in file:
print(type(riga))
mostra:
<class 'str'>
Per trasformarli in interi:
with open("numeri.txt", "r", encoding="utf-8") as file:
numeri = [
int(riga.strip())
for riga in file
if riga.strip()
]
Ora:
print(numeri)
potrebbe produrre:
[10, 20, 30]
Possiamo utilizzare le funzioni built-in:
with open("numeri.txt", "r", encoding="utf-8") as file:
numeri = [int(riga) for riga in file if riga.strip()]
Poi:
if numeri:
print("Somma:", sum(numeri))
print("Media:", sum(numeri) / len(numeri))
print("Minimo:", min(numeri))
print("Massimo:", max(numeri))
FILE = "numeri.txt"
with open(FILE, "r", encoding="utf-8") as file:
numeri = [
int(riga.strip())
for riga in file
if riga.strip()
]
if numeri:
totale = sum(numeri)
media = totale / len(numeri)
print(f"File: {FILE}")
print(f"Numeri: {len(numeri)}")
print(f"Somma: {totale}")
print(f"Media: {media:.2f}")
print(f"Minimo: {min(numeri)}")
print(f"Massimo: {max(numeri)}")
else:
print("Nessun numero presente")
La soluzione:
int(riga.strip())
genera ValueError se una riga non contiene un intero valido.
Possiamo gestire la situazione:
numeri = []
with open("numeri.txt", "r", encoding="utf-8") as file:
for riga in file:
riga = riga.strip()
if not riga:
continue
try:
numeri.append(int(riga))
except ValueError:
print(f"Riga non valida: {riga!r}")
Qui utilizziamo direttamente le tecniche del capitolo 6.
Possiamo contare le occorrenze di un carattere:
with open("testo.txt", "r", encoding="utf-8") as file:
contenuto = file.read()
carattere = input("Carattere da cercare: ")
print(
f"Il carattere {carattere!r} "
f"compare {contenuto.count(carattere)} volte"
)
Oppure possiamo contare manualmente:
conteggio = sum(
1
for carattere_file in contenuto
if carattere_file == carattere
)
La seconda versione è utile dal punto di vista didattico perché mostra esplicitamente il processo di scansione.
La prima è normalmente preferibile quando ci basta il risultato.
Quando lavoriamo con testo, è importante conoscere l'encoding.
Un file contiene byte. L'encoding stabilisce come quei byte rappresentano caratteri.
Per esempio:
with open(
"testo.txt",
"r",
encoding="utf-8"
) as file:
contenuto = file.read()
Per la scrittura:
with open(
"testo.txt",
"w",
encoding="utf-8"
) as file:
file.write("Città: Torino\n")
Per evitare dipendenze dal sistema operativo o dalla configurazione locale.
In particolare, specificare esplicitamente:
encoding="utf-8"
rende il comportamento del programma più prevedibile.
Se un file è stato scritto con un encoding incompatibile con quello utilizzato per leggerlo, Python può generare:
UnicodeDecodeError
Questo è un problema diverso da:
FileNotFoundError
Il primo riguarda la decodifica del contenuto; il secondo riguarda l'assenza del file.
Quando lavoriamo con dati esterni, dobbiamo quindi considerare:
percorso corretto
+
file esistente
+
encoding corretto
pathlib: lavorare con i percorsiPython offre il modulo:
pathlib
per lavorare con i percorsi in modo moderno e portabile.
from pathlib import Path
path = Path("dati.txt")
Possiamo verificare l'esistenza:
if path.exists():
print("Il file esiste")
Verificare se è un file:
if path.is_file():
print("È un file")
Verificare se è una directory:
if path.is_dir():
print("È una directory")
PathPer file di testo piccoli possiamo utilizzare direttamente:
from pathlib import Path
path = Path("dati.txt")
path.write_text(
"Ciao Python!\n",
encoding="utf-8"
)
Lettura:
contenuto = path.read_text(encoding="utf-8")
print(contenuto)
Questa sintassi è molto comoda.
È particolarmente adatta quando:
Per file grandi, l'uso di:
open()
e l'iterazione riga per riga può essere più appropriato.
PathInvece di concatenare stringhe:
"data/" + nome_file
possiamo utilizzare:
from pathlib import Path
cartella = Path("data")
file = cartella / "studenti.txt"
L'operatore / viene sovraccaricato da Path per costruire percorsi.
Esempio:
path = Path("data") / "2026" / "studenti.txt"
Questo è più portabile rispetto a costruire manualmente stringhe con /
o \.
Possiamo creare una directory:
from pathlib import Path
cartella = Path("output")
cartella.mkdir(exist_ok=True)
exist_ok=True evita un errore se la directory esiste già.
Per creare anche eventuali directory intermedie:
cartella = Path("output") / "report" / "2026"
cartella.mkdir(parents=True, exist_ok=True)
Con Path possiamo utilizzare:
from pathlib import Path
cartella = Path(".")
for elemento in cartella.iterdir():
print(elemento)
Possiamo filtrare:
for file in cartella.glob("*.txt"):
print(file)
Oppure cercare ricorsivamente:
for file in cartella.rglob("*.txt"):
print(file)
glob("*.txt")
cerca secondo il pattern nella directory indicata.
rglob("*.txt")
effettua la ricerca anche nelle sottodirectory.
Un Path può fornire informazioni utili.
from pathlib import Path
path = Path("dati.txt")
print(path.name)
print(path.stem)
print(path.suffix)
print(path.parent)
Se il percorso è:
data/studenti.csv
potremmo ottenere:
name → studenti.csv
stem → studenti
suffix → .csv
parent → data
Questo è molto utile per programmi che devono analizzare file presenti in una directory.
Possiamo eliminare un file:
path.unlink()
Possiamo rinominarlo:
path.rename("nuovo_nome.txt")
Oppure spostarlo utilizzando un nuovo percorso:
path.rename(Path("archivio") / path.name)
Le operazioni di modifica del filesystem devono essere eseguite con attenzione.
Per esempio:
path.unlink()
elimina realmente il file.
Prima di eseguire operazioni distruttive è buona pratica verificare che il percorso sia quello previsto.
Il File I/O è un ottimo esempio di situazione in cui le eccezioni sono fondamentali.
from pathlib import Path
path = Path("dati.txt")
try:
contenuto = path.read_text(encoding="utf-8")
except FileNotFoundError:
print("File non trovato")
except UnicodeDecodeError:
print("Encoding non compatibile")
Possiamo anche gestire:
PermissionError
quando non abbiamo i permessi necessari.
open()try:
with open("dati.txt", "r", encoding="utf-8") as file:
contenuto = file.read()
except FileNotFoundError:
print("Il file non esiste")
except PermissionError:
print("Permesso negato")
FileNotFoundError e PathPossiamo controllare prima:
if path.exists():
contenuto = path.read_text(encoding="utf-8")
else:
print("File non trovato")
oppure utilizzare EAFP:
try:
contenuto = path.read_text(encoding="utf-8")
except FileNotFoundError:
print("File non trovato")
Entrambi gli approcci possono essere appropriati.
Se l'operazione di lettura è comunque il passo naturale e vogliamo gestire il fallimento, l'approccio con eccezione può essere molto chiaro.
Un formato molto comune per lo scambio di dati è il CSV:
Comma-Separated Values
Esempio:
nome,eta,voto
Anna,20,8
Marco,22,7
Luca,21,9
Python mette a disposizione il modulo:
csv
Non è necessario analizzare manualmente le virgole.
import csv
with open(
"studenti.csv",
"r",
newline="",
encoding="utf-8"
) as file:
reader = csv.reader(file)
for riga in reader:
print(riga)
Possiamo ottenere:
['nome', 'eta', 'voto']
['Anna', '20', '8']
['Marco', '22', '7']
newline=""?È la modalità consigliata dalla documentazione del modulo csv quando
si apre un file per essere gestito da csv.reader o csv.writer,
evitando problemi legati alla gestione delle terminazioni di riga.
DictReaderSe il CSV contiene un'intestazione, possiamo usare:
import csv
with open(
"studenti.csv",
"r",
newline="",
encoding="utf-8"
) as file:
reader = csv.DictReader(file)
for studente in reader:
print(studente)
Ogni riga diventa un dizionario.
Per esempio:
{
"nome": "Anna",
"eta": "20",
"voto": "8"
}
Questo è spesso più leggibile.
I valori letti dal CSV sono normalmente stringhe.
Quindi:
studente["eta"]
è una stringa, non un int.
Possiamo convertire:
eta = int(studente["eta"])
voto = int(studente["voto"])
Con csv.writer:
import csv
with open(
"studenti.csv",
"w",
newline="",
encoding="utf-8"
) as file:
writer = csv.writer(file)
writer.writerow(["nome", "eta", "voto"])
writer.writerow(["Anna", 20, 8])
writer.writerow(["Marco", 22, 7])
studenti = [
["Anna", 20, 8],
["Marco", 22, 7],
["Luca", 21, 9],
]
with open(
"studenti.csv",
"w",
newline="",
encoding="utf-8"
) as file:
writer = csv.writer(file)
writer.writerow(["nome", "eta", "voto"])
writer.writerows(studenti)
DictWriterPossiamo anche lavorare con dizionari:
import csv
studenti = [
{"nome": "Anna", "eta": 20, "voto": 8},
{"nome": "Marco", "eta": 22, "voto": 7},
]
with open(
"studenti.csv",
"w",
newline="",
encoding="utf-8"
) as file:
fieldnames = ["nome", "eta", "voto"]
writer = csv.DictWriter(
file,
fieldnames=fieldnames
)
writer.writeheader()
writer.writerows(studenti)
Questo si integra molto bene con le strutture dati viste nei capitoli precedenti.
Crea un programma che:
studenti.csv;DictReader;Il formato JSON è molto importante nello sviluppo software moderno.
JSON significa:
JavaScript Object Notation
È un formato testuale utilizzato per rappresentare dati strutturati.
Esempio:
{
"nome": "Anna",
"eta": 25,
"studente": true
}
Python mette a disposizione il modulo:
json
Esiste una corrispondenza naturale tra molti tipi Python e JSON.
Python JSON
dict object
list array
str string
int / float number
True true
False false
None null
Questo rende JSON molto utile per serializzare strutture dati.
json.dump()import json
dati = {
"nome": "Mario",
"eta": 25,
"linguaggi": [
"Python",
"Java"
]
}
with open(
"utente.json",
"w",
encoding="utf-8"
) as file:
json.dump(
dati,
file,
ensure_ascii=False,
indent=4
)
Il parametro:
indent=4
rende il file leggibile.
ensure_ascii=False permette di mantenere più naturalmente i caratteri
Unicode nel file.
json.load()import json
with open(
"utente.json",
"r",
encoding="utf-8"
) as file:
dati = json.load(file)
print(dati)
Ora dati è nuovamente un oggetto Python, tipicamente un dizionario.
Possiamo accedere:
print(dati["nome"])
print(dati["eta"])
dumps() e loads()Il modulo json distingue tra operazioni su file e operazioni su
stringhe.
dump()Scrive JSON in un file:
json.dump(dati, file)
load()Legge JSON da un file:
dati = json.load(file)
dumps()Converte un oggetto Python in una stringa JSON:
testo_json = json.dumps(dati)
loads()Converte una stringa JSON in un oggetto Python:
dati = json.loads(testo_json)
Una regola mnemonica:
dump → file
dumps → stringa
load → file
loads → stringa
Possiamo salvare strutture molto più ricche:
dati = {
"corso": "Python",
"docente": {
"nome": "Mario",
"materie": [
"Python",
"Java"
]
},
"studenti": [
{
"nome": "Anna",
"voto": 8
},
{
"nome": "Marco",
"voto": 7
}
]
}
Scrittura:
with open(
"corso.json",
"w",
encoding="utf-8"
) as file:
json.dump(
dati,
file,
ensure_ascii=False,
indent=4
)
Questo è uno dei motivi per cui JSON è così diffuso.
Un JSON non contiene direttamente un'istanza arbitraria di una nostra classe.
Per esempio:
class Persona:
...
non può essere passato automaticamente a:
json.dump(persona, file)
senza definire come rappresentarlo.
Possiamo invece trasformarlo in una struttura serializzabile:
dati = {
"nome": persona.nome,
"eta": persona.eta
}
oppure utilizzare dataclasses.asdict() per una dataclass.
from dataclasses import dataclass, asdict
import json
@dataclass
class Persona:
nome: str
eta: int
persona = Persona("Anna", 25)
with open("persona.json", "w", encoding="utf-8") as file:
json.dump(
asdict(persona),
file,
ensure_ascii=False,
indent=4
)
Questo concetto di trasformazione tra oggetti e rappresentazioni persistenti diventerà molto importante nelle applicazioni reali.
Finora abbiamo lavorato con testo.
Esistono però file costituiti direttamente da byte:
Per lavorare in modalità binaria utilizziamo:
"rb"
per leggere e:
"wb"
per scrivere.
Esempio:
with open("immagine.png", "rb") as file:
dati = file.read()
dati sarà un oggetto:
bytes
non una stringa.
Possiamo copiare un file leggendo byte e scrivendoli:
with open("origine.bin", "rb") as origine:
with open("copia.bin", "wb") as destinazione:
destinazione.write(origine.read())
Per file grandi è preferibile lavorare a blocchi:
with open("origine.bin", "rb") as origine:
with open("copia.bin", "wb") as destinazione:
while blocco := origine.read(4096):
destinazione.write(blocco)
L'operatore := è il walrus operator e permette di assegnare e
valutare il blocco nella stessa espressione.
Una versione più esplicita, spesso utile a scopo didattico, è:
with open("origine.bin", "rb") as origine:
with open("copia.bin", "wb") as destinazione:
while True:
blocco = origine.read(4096)
if not blocco:
break
destinazione.write(blocco)
read()Questo codice:
with open("file_enorme.txt", "r", encoding="utf-8") as file:
contenuto = file.read()
carica l'intero file in memoria.
Per un file molto grande può essere inefficiente.
Meglio:
with open("file_enorme.txt", "r", encoding="utf-8") as file:
for riga in file:
elabora(riga)
Il programma lavora progressivamente.
Per file piccoli:
read()
è semplice e comodo.
Per file grandi:
for riga in file:
è spesso più appropriato.
Supponiamo di avere:
INFO Avvio
ERROR Connessione fallita
INFO Nuovo utente
WARNING Memoria quasi piena
ERROR Timeout
Possiamo estrarre solo gli errori:
with open("app.log", "r", encoding="utf-8") as file:
for riga in file:
if "ERROR" in riga:
print(riga, end="")
Possiamo anche salvare il risultato:
with (
open("app.log", "r", encoding="utf-8") as origine,
open("errori.log", "w", encoding="utf-8") as destinazione
):
for riga in origine:
if "ERROR" in riga:
destinazione.write(riga)
Questa sintassi permette di utilizzare più context manager nello stesso
with.
Supponiamo di voler creare una versione in maiuscolo:
with open("origine.txt", "r", encoding="utf-8") as origine:
with open("maiuscolo.txt", "w", encoding="utf-8") as destinazione:
for riga in origine:
destinazione.write(riga.upper())
Possiamo naturalmente applicare qualsiasi trasformazione:
Riprendiamo un esercizio sulla manipolazione delle stringhe.
Possiamo leggere un file:
with open("testo.txt", "r", encoding="utf-8") as file:
righe = file.readlines()
e trasformare ogni riga:
nuove_righe = []
for riga in righe:
parole = riga.split()
nuova_riga = " ".join(
parola.capitalize()
for parola in parole
)
nuove_righe.append(nuova_riga + "\n")
Poi:
with open("testo_capitalizzato.txt", "w", encoding="utf-8") as file:
file.writelines(nuove_righe)
Questo esempio collega:
stringhe
+
generator expression
+
File I/O
È spesso una buona idea separare il codice di accesso ai file.
Per esempio:
def leggi_testo(percorso):
with open(percorso, "r", encoding="utf-8") as file:
return file.read()
e:
def scrivi_testo(percorso, contenuto):
with open(percorso, "w", encoding="utf-8") as file:
file.write(contenuto)
Uso:
testo = leggi_testo("dati.txt")
scrivi_testo(
"copia.txt",
testo
)
Questo evita di ripetere continuamente la stessa logica.
Possiamo costruire una piccola API:
import json
def salva_json(percorso, dati):
with open(
percorso,
"w",
encoding="utf-8"
) as file:
json.dump(
dati,
file,
ensure_ascii=False,
indent=4
)
def carica_json(percorso):
with open(
percorso,
"r",
encoding="utf-8"
) as file:
return json.load(file)
Uso:
dati = {
"nome": "Anna",
"eta": 25
}
salva_json("utente.json", dati)
utente = carica_json("utente.json")
print(utente)
Abbiamo creato una piccola API di persistenza.
Possiamo decidere di lasciare propagare le eccezioni:
def carica_json(percorso):
with open(
percorso,
"r",
encoding="utf-8"
) as file:
return json.load(file)
Il chiamante può gestirle:
try:
dati = carica_json("config.json")
except FileNotFoundError:
print("Configurazione assente")
except json.JSONDecodeError:
print("JSON non valido")
Questa soluzione mantiene separati:
funzione di persistenza
│
▼
lettura del dato
│
▼
codice chiamante
│
▼
decisione su come gestire l'errore
È spesso preferibile rispetto a inserire messaggi print() direttamente
nelle funzioni riutilizzabili.
Se il file contiene:
{
"nome": "Anna",
"eta":
}
la lettura:
json.load(file)
genera:
json.JSONDecodeError
Possiamo gestirlo:
try:
with open(
"dati.json",
"r",
encoding="utf-8"
) as file:
dati = json.load(file)
except json.JSONDecodeError:
print("Il file JSON non è valido")
Questo è un esempio perfetto di integrazione tra:
Possiamo utilizzare una classe per astrarre la persistenza.
import json
class RepositoryStudenti:
def __init__(self, percorso):
self.percorso = percorso
def salva(self, studenti):
with open(
self.percorso,
"w",
encoding="utf-8"
) as file:
json.dump(
studenti,
file,
ensure_ascii=False,
indent=4
)
def carica(self):
with open(
self.percorso,
"r",
encoding="utf-8"
) as file:
return json.load(file)
Uso:
studenti = [
{"nome": "Anna", "voto": 8},
{"nome": "Marco", "voto": 7}
]
repository = RepositoryStudenti("studenti.json")
repository.salva(studenti)
studenti_caricati = repository.carica()
print(studenti_caricati)
Questo esempio collega direttamente:
OOP
+
File I/O
+
JSON
e anticipa alcuni concetti architetturali che incontreremo nello sviluppo di applicazioni più grandi.
Path nel repositoryPossiamo migliorare la classe utilizzando Path:
from pathlib import Path
import json
class RepositoryStudenti:
def __init__(self, percorso):
self.percorso = Path(percorso)
def salva(self, studenti):
self.percorso.write_text(
json.dumps(
studenti,
ensure_ascii=False,
indent=4
),
encoding="utf-8"
)
def carica(self):
return json.loads(
self.percorso.read_text(
encoding="utf-8"
)
)
La soluzione è compatta e adatta a file JSON piccoli.
Per file grandi o quando vogliamo lavorare progressivamente, può essere
preferibile utilizzare direttamente open().
Quando un percorso proviene dall'utente dobbiamo prestare attenzione.
Esempio:
nome = input("Nome file: ")
path = Path(nome)
Non dobbiamo assumere automaticamente che il percorso sia sicuro o appropriato.
In applicazioni reali bisogna considerare:
Il semplice utilizzo di:
Path(nome)
non rende automaticamente sicuro un percorso proveniente da input esterno.
close()Da evitare:
file = open("dati.txt")
contenuto = file.read()
Meglio:
with open("dati.txt", encoding="utf-8") as file:
contenuto = file.read()
"w" pensando significhi appendopen("dati.txt", "w")
sovrascrive.
Per aggiungere:
open("dati.txt", "a")
\nfile.write("Anna")
file.write("Marco")
produce:
AnnaMarco
Se vogliamo due righe:
file.write("Anna\n")
file.write("Marco\n")
Per il testo è buona pratica specificare:
encoding="utf-8"
read()Per file grandi può essere preferibile:
for riga in file:
...
Anche se rappresentano dati strutturati, i valori letti devono essere interpretati correttamente.
Per esempio:
"25"
non è automaticamente:
25
Realizza un programma che gestisca un registro di studenti.
Parti da:
studenti = [
{"nome": "Anna", "voto": 8},
{"nome": "Marco", "voto": 7},
{"nome": "Luca", "voto": 9},
]
Il programma deve:
studenti.json;import json
studenti = [
{"nome": "Anna", "voto": 8},
{"nome": "Marco", "voto": 7},
{"nome": "Luca", "voto": 9},
]
with open(
"studenti.json",
"w",
encoding="utf-8"
) as file:
json.dump(
studenti,
file,
ensure_ascii=False,
indent=4
)
try:
with open(
"studenti.json",
"r",
encoding="utf-8"
) as file:
studenti = json.load(file)
except FileNotFoundError:
print("File non trovato")
else:
voti = [studente["voto"] for studente in studenti]
if voti:
media = sum(voti) / len(voti)
print(f"Media: {media:.2f}")
print(f"Massimo: {max(voti)}")
ordinati = sorted(
studenti,
key=lambda studente: studente["voto"],
reverse=True
)
for studente in ordinati:
print(
studente["nome"],
studente["voto"]
)
Qui abbiamo integrato:
open();with;sorted();lambda.open()?"r" e "w"?"w" e "a"?"x"?with open(...)?read()?readline()?for riga in file?readlines()?tell() e seek()?write() aggiunge automaticamente \n?writelines() aggiunge automaticamente \n?encoding="utf-8"?pathlib.Path?json.dump() e json.dumps()?json.load() e json.loads()?"r" legge; "w" scrive sostituendo il contenuto."w" sovrascrive; "a" aggiunge in coda.dump() scrive su file; dumps() restituisce una stringa JSON.load() legge da file; loads() legge da una stringa JSON.Crea un programma che chieda all'utente una frase e la aggiunga a:
diario.txt
Ogni frase deve occupare una nuova riga.
Scrivi un programma che legga un file contenente un numero intero per riga e produca:
Gestisci:
Dato un file di testo, crea:
filtrato.txt
contenente solo le righe che includono una parola scelta dall'utente.
Crea un CSV con:
nome,cognome,voto
Scrivi almeno cinque studenti.
Poi rileggi il file con DictReader e calcola la media.
Crea un file JSON contenente una lista di prodotti:
[
{
"codice": "P01",
"nome": "Mouse",
"prezzo": 25.90
},
...
]
Scrivi un programma che:
pathlibCrea una directory:
report/
e genera al suo interno tre file:
report1.txt
report2.txt
report3.txt
Poi usa glob() per trovare tutti i file .txt.
Realizza una piccola applicazione per gestire un registro studenti.
Il programma deve utilizzare JSON come sistema di persistenza.
Ogni studente contiene:
{
"id": 1,
"nome": "Anna",
"cognome": "Rossi",
"voti": [8, 7, 9]
}
All'avvio:
carica_studenti()
deve leggere:
studenti.json
Se il file non esiste, deve restituire una lista vuota.
Definisci:
salva_studenti(studenti)
che salvi la lista nel file JSON.
Definisci:
aggiungi_studente(studenti, studente)
L'ID deve essere univoco.
Definisci:
cerca_studente(studenti, testo)
La ricerca deve considerare:
La ricerca non deve distinguere maiuscole e minuscole.
Definisci:
media_studente(studente)
che restituisca la media dei voti.
Definisci:
classifica(studenti)
che restituisca gli studenti ordinati per media decrescente.
Definisci eccezioni personalizzate:
class RegistroError(Exception):
pass
class StudenteNonTrovatoError(RegistroError):
pass
class StudenteGiaPresenteError(RegistroError):
pass
Utilizzale dove appropriato.
Realizza un semplice menu:
1. Elenca studenti
2. Aggiungi studente
3. Cerca studente
4. Aggiungi voto
5. Classifica
6. Salva
0. Esci
Il programma deve rimanere in esecuzione fino alla scelta:
0
Una possibile organizzazione è:
registro.py
studenti.json
class RegistroError(Exception):
pass
class StudenteNonTrovatoError(RegistroError):
pass
class StudenteGiaPresenteError(RegistroError):
pass
import json
FILE = "studenti.json"
def carica_studenti():
try:
with open(
FILE,
"r",
encoding="utf-8"
) as file:
return json.load(file)
except FileNotFoundError:
return []
def salva_studenti(studenti):
with open(
FILE,
"w",
encoding="utf-8"
) as file:
json.dump(
studenti,
file,
ensure_ascii=False,
indent=4
)
def trova_per_id(studenti, id_studente):
for studente in studenti:
if studente["id"] == id_studente:
return studente
raise StudenteNonTrovatoError(
f"Studente {id_studente} non trovato"
)
def aggiungi_studente(studenti, studente):
try:
trova_per_id(studenti, studente["id"])
except StudenteNonTrovatoError:
studenti.append(studente)
return
raise StudenteGiaPresenteError(
f"ID {studente['id']} già presente"
)
Qui l'eccezione viene utilizzata come parte della logica di ricerca.
Una possibile alternativa sarebbe controllare direttamente con any():
if any(
s["id"] == studente["id"]
for s in studenti
):
raise StudenteGiaPresenteError(
f"ID {studente['id']} già presente"
)
studenti.append(studente)
In questo caso l'eccezione rappresenta una vera condizione di errore dell'operazione di inserimento.
def cerca_studente(studenti, testo):
testo = testo.lower()
return [
studente
for studente in studenti
if testo in studente["nome"].lower()
or testo in studente["cognome"].lower()
]
def media_studente(studente):
voti = studente["voti"]
if not voti:
return None
return sum(voti) / len(voti)
Restituire None quando non ci sono voti permette di distinguere:
media = 0
da:
nessun voto disponibile
def classifica(studenti):
return sorted(
studenti,
key=lambda studente: (
media_studente(studente)
if media_studente(studente) is not None
else -1
),
reverse=True
)
Una versione più leggibile evita di calcolare due volte la media utilizzando una funzione di supporto.
La soluzione della sfida funziona, ma in un'applicazione più grande possiamo separare:
Student
│
▼
Registro
│
▼
Repository
│
▼
studenti.json
Per esempio:
class StudentiRepository:
def __init__(self, percorso):
self.percorso = percorso
def carica(self):
...
def salva(self, studenti):
...
Il registro invece si occupa delle regole:
class RegistroStudenti:
def __init__(self, repository):
self.repository = repository
self.studenti = repository.carica()
def aggiungi(self, studente):
...
def cerca(self, testo):
...
def classifica(self):
...
Questa separazione permette di cambiare la persistenza senza riscrivere la logica del registro.
Per esempio, potremmo passare da:
JSON
a:
CSV
o successivamente a:
database
senza modificare necessariamente tutto il programma.
Questo principio sarà fondamentale quando passeremo alla progettazione di applicazioni più strutturate.
I file sono ottimi per:
Ma quando i dati diventano grandi o devono essere utilizzati contemporaneamente da più utenti, possiamo avere bisogno di un database.
Il percorso concettuale è:
Variabili
↓
File di testo
↓
CSV / JSON
↓
Database
↓
Applicazioni reali
Il File I/O rappresenta quindi un passaggio fondamentale tra la programmazione in memoria e la gestione della persistenza nelle applicazioni professionali.
Abbiamo imparato a gestire l'input/output su file.
La forma base è:
with open(
"dati.txt",
"r",
encoding="utf-8"
) as file:
contenuto = file.read()
Le modalità principali sono:
r → lettura
w → scrittura/sovrascrittura
a → append
x → creazione esclusiva
b → binario
+ → lettura e scrittura
Per leggere possiamo usare:
read()
readline()
readlines()
oppure, spesso preferibilmente:
for riga in file:
...
Per scrivere:
write()
e:
writelines()
Per i percorsi abbiamo introdotto:
from pathlib import Path
Per dati tabellari:
csv
Per dati strutturati:
json
Per file binari:
rb
wb
Abbiamo inoltre integrato il File I/O con le eccezioni:
try:
...
except FileNotFoundError:
...
e con la programmazione orientata agli oggetti:
class Repository:
...
withwith open(...) as file:
...
encoding="utf-8"
w e aw → sostituisce
a → aggiunge
Preferisci:
for riga in file:
...
quando puoi elaborare i dati progressivamente.
pathlib per i percorsipath = Path("data") / "studenti.json"
CSV:
csv.reader()
csv.DictReader()
csv.writer()
csv.DictWriter()
JSON:
json.load()
json.dump()
try:
...
except FileNotFoundError:
...
Una classe o funzione che salva i dati non dovrebbe necessariamente conoscere tutte le regole del dominio.
Nel prossimo capitolo cambieremo prospettiva.
Finora abbiamo utilizzato:
La programmazione funzionale ci permetterà di ragionare sempre più in termini di:
dati → trasformazione → risultato
Approfondiremo:
lambda;map();filter();reduce();sorted() con key;zip();Un esempio molto semplice è già comparso in questo capitolo:
numeri = [1, 2, 3, 4, 5]
quadrati = list(
map(
lambda n: n ** 2,
numeri
)
)
La domanda non sarà più soltanto:
"Come faccio a scrivere un ciclo?"
ma anche:
"Quale trasformazione voglio applicare ai miei dati?"
Questo ci porterà naturalmente verso un modo diverso di progettare le soluzioni Python.