1 / 1
Indice della lezione

Capitolo 7 - File I/O: leggere e scrivere dati

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:

  • leggere e scrivere file di testo;
  • comprendere modalità e puntatore del file;
  • usare with;
  • leggere un file interamente o riga per riga;
  • scrivere e aggiungere contenuto;
  • specificare correttamente l'encoding;
  • gestire i file con pathlib;
  • lavorare con file CSV;
  • lavorare con file JSON;
  • gestire file binari;
  • integrare File I/O ed eccezioni;
  • progettare funzioni riutilizzabili per la persistenza.

Prerequisiti: funzioni, strutture dati, comprehension, classi ed eccezioni.


7.1 Input e output

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ò:

  • leggere dati da un file;
  • elaborare i dati;
  • scrivere risultati in un altro file.

7.2 Perché usare i file?

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:

  • configurazioni;
  • risultati;
  • registri;
  • dati esportati;
  • log;
  • documenti;
  • immagini;
  • dati strutturati;
  • informazioni da scambiare con altri programmi.

I file sono quindi uno dei primi strumenti attraverso cui un programma può interagire con dati persistenti.


7.3 La funzione 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.


7.4 Modalità principali di apertura

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.

Modalità predefinita

Se non specifichiamo la modalità:

open("dati.txt")

Python utilizza:

"r"

7.5 Modalità "r" --- lettura

with open("dati.txt", "r") as file:
    contenuto = file.read()

print(contenuto)

Il file deve esistere.

Se non esiste:

FileNotFoundError

Con encoding

Per 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.


7.6 Modalità "w" --- scrittura

with 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.

Attenzione

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

7.7 Modalità "a" --- append

Se 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:

  • log;
  • registri;
  • cronologie;
  • file costruiti progressivamente.

Esercizio 7.1

Scrivi un programma che:

  1. crei saluto.txt;
  2. scriva "Ciao Python!";
  3. riapra il file in append;
  4. aggiunga "Sto imparando il File I/O.".

Poi leggi e stampa il contenuto.


7.8 Modalità "x" --- creazione esclusiva

La 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.


7.9 Perché usare with

La 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()

Perché è importante?

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.


7.10 Leggere tutto il file con 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.


7.11 Il puntatore del file

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())

7.12 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.


7.13 Leggere riga per riga con 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:

""

Perché 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.


7.14 Leggere con un ciclo for

In 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 != "":

Confronto

Più verboso:

riga = file.readline()

while riga != "":
    ...
    riga = file.readline()

Più Pythonico:

for riga in file:
    ...

Esercizio 7.2

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

7.15 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"
]

Attenzione alla memoria

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.


7.16 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.


7.17 Scrivere con 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 \n

Questa istruzione:

file.write("Ciao")

scrive esattamente:

Ciao

Per andare a capo dobbiamo scrivere:

file.write("Ciao\n")

Valore restituito

write() restituisce normalmente il numero di caratteri scritti.

with open("output.txt", "w", encoding="utf-8") as file:
    n = file.write("Ciao")
    print(n)

7.18 Scrivere più righe

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().


7.19 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)

Attenzione

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)

7.20 Scrivere dati generati dal programma

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:

  • input;
  • conversione;
  • ciclo;
  • generazione casuale;
  • File I/O;
  • f-string;
  • context manager.

7.21 Leggere numeri da un file

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]

7.22 Analizzare un file numerico

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))

Esempio completo

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")

7.23 Validare righe numeriche

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.


7.24 Un programma di analisi dei caratteri

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.


7.25 Encoding

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")

Perché specificarlo?

Per evitare dipendenze dal sistema operativo o dalla configurazione locale.

In particolare, specificare esplicitamente:

encoding="utf-8"

rende il comportamento del programma più prevedibile.


7.26 Errori di encoding

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

7.27 pathlib: lavorare con i percorsi

Python 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")

7.28 Leggere e scrivere con Path

Per 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.

Quando usarla?

È particolarmente adatta quando:

  • il file è piccolo;
  • vogliamo leggere o scrivere tutto il contenuto;
  • non abbiamo bisogno di elaborare progressivamente le righe.

Per file grandi, l'uso di:

open()

e l'iterazione riga per riga può essere più appropriato.


7.29 Costruire percorsi con Path

Invece 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 \.


7.30 Directory

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)

7.31 Elencare i file di una directory

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)

Differenza

glob("*.txt")

cerca secondo il pattern nella directory indicata.

rglob("*.txt")

effettua la ricerca anche nelle sottodirectory.


7.32 Informazioni su un file

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.


7.33 Eliminare e rinominare file

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)

Attenzione

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.


7.34 Gestire gli errori di File I/O

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.

Esempio con 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")

7.35 FileNotFoundError e Path

Possiamo 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.


7.36 CSV --- dati tabellari

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.


7.37 Leggere un CSV

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']

Perché 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.


7.38 DictReader

Se 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.

Attenzione ai tipi

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"])

7.39 Scrivere un CSV

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])

Più righe

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)

7.40 DictWriter

Possiamo 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.

Esercizio 7.3

Crea un programma che:

  1. contenga una lista di dizionari con nome, età e voto;
  2. salvi i dati in studenti.csv;
  3. rilegga il CSV con DictReader;
  4. calcoli la media dei voti.

7.41 JSON

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

7.42 Python e 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.


7.43 Scrivere JSON con 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.


7.44 Leggere JSON con 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"])

7.45 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

7.46 JSON con strutture annidate

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.


7.47 JSON e oggetti Python

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.


7.48 File binari

Finora abbiamo lavorato con testo.

Esistono però file costituiti direttamente da byte:

  • immagini;
  • PDF;
  • file audio;
  • archivi;
  • alcuni formati proprietari.

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.


7.49 Copiare un file binario

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)

7.50 File grandi: evitare 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.

Regola pratica

Per file piccoli:

read()

è semplice e comodo.

Per file grandi:

for riga in file:

è spesso più appropriato.


7.51 Esempio: filtrare un file

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.


7.52 Esempio: trasformare un file

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:

  • filtraggio;
  • normalizzazione;
  • sostituzione;
  • conversione;
  • analisi.

7.53 Esempio: capitalizzare le parole

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

7.54 Funzioni riutilizzabili per il 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.


7.55 Funzioni per JSON

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.


7.56 Gestire gli errori nelle funzioni 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.


7.57 JSON non valido

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:

  • File I/O;
  • librerie standard;
  • eccezioni.

7.58 Un piccolo repository JSON

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.


7.59 Path nel repository

Possiamo 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().


7.60 File I/O e sicurezza

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:

  • permessi;
  • directory consentite;
  • file esistenti;
  • nomi validi;
  • path traversal;
  • dimensione dei file;
  • encoding;
  • dati non attendibili.

Il semplice utilizzo di:

Path(nome)

non rende automaticamente sicuro un percorso proveniente da input esterno.


7.61 Errori comuni nel File I/O

Errore 1 --- dimenticare close()

Da evitare:

file = open("dati.txt")
contenuto = file.read()

Meglio:

with open("dati.txt", encoding="utf-8") as file:
    contenuto = file.read()

Errore 2 --- usare "w" pensando significhi append

open("dati.txt", "w")

sovrascrive.

Per aggiungere:

open("dati.txt", "a")

Errore 3 --- dimenticare \n

file.write("Anna")
file.write("Marco")

produce:

AnnaMarco

Se vogliamo due righe:

file.write("Anna\n")
file.write("Marco\n")

Errore 4 --- dimenticare l'encoding

Per il testo è buona pratica specificare:

encoding="utf-8"

Errore 5 --- leggere un file enorme con read()

Per file grandi può essere preferibile:

for riga in file:
    ...

Errore 6 --- dimenticare che CSV e JSON sono testo

Anche se rappresentano dati strutturati, i valori letti devono essere interpretati correttamente.

Per esempio:

"25"

non è automaticamente:

25

7.62 Esercizio integrato --- Registro voti

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:

  1. salvare i dati in studenti.json;
  2. ricaricarli;
  3. calcolare la media;
  4. trovare il voto massimo;
  5. stampare gli studenti ordinati per voto;
  6. gestire il caso in cui il file non esista.

7.63 Soluzione

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:

  • dizionari;
  • liste;
  • JSON;
  • open();
  • with;
  • eccezioni;
  • comprehension;
  • sorted();
  • lambda.

7.64 Mini-verifica

Domande

  1. Che cosa significa I/O?
  2. A cosa serve open()?
  3. Qual è la differenza tra "r" e "w"?
  4. Qual è la differenza tra "w" e "a"?
  5. A cosa serve "x"?
  6. Perché è consigliato usare with open(...)?
  7. Che cosa restituisce read()?
  8. Che cosa fa readline()?
  9. Qual è il vantaggio di for riga in file?
  10. Che cosa fa readlines()?
  11. A cosa servono tell() e seek()?
  12. write() aggiunge automaticamente \n?
  13. writelines() aggiunge automaticamente \n?
  14. Perché è utile specificare encoding="utf-8"?
  15. A cosa serve pathlib.Path?
  16. Che differenza c'è tra json.dump() e json.dumps()?
  17. Che differenza c'è tra json.load() e json.loads()?
  18. Perché i valori letti da CSV sono normalmente stringhe?
  19. Quando conviene leggere un file riga per riga?
  20. Che cosa distingue un file di testo da un file binario?

Risposte sintetiche

  1. Input/Output.
  2. Per aprire una risorsa file.
  3. "r" legge; "w" scrive sostituendo il contenuto.
  4. "w" sovrascrive; "a" aggiunge in coda.
  5. Per creare un nuovo file senza sovrascriverne uno esistente.
  6. Per garantire la corretta gestione e chiusura della risorsa.
  7. Una stringa contenente il contenuto letto.
  8. Legge una riga dalla posizione corrente.
  9. Permette una lettura semplice e progressiva.
  10. Restituisce una lista di righe.
  11. Leggono e modificano la posizione corrente nel file.
  12. No.
  13. No.
  14. Per rendere esplicita e prevedibile la codifica del testo.
  15. Per rappresentare e manipolare percorsi in modo portabile.
  16. dump() scrive su file; dumps() restituisce una stringa JSON.
  17. load() legge da file; loads() legge da una stringa JSON.
  18. Perché il CSV è un formato testuale.
  19. Soprattutto con file grandi o quando elaboriamo le righe progressivamente.
  20. Il file binario viene trattato come sequenza di byte; quello di testo viene decodificato in caratteri.

7.65 Esercizi di riepilogo

Esercizio 1 --- Diario

Crea un programma che chieda all'utente una frase e la aggiunga a:

diario.txt

Ogni frase deve occupare una nuova riga.


Esercizio 2 --- Statistiche

Scrivi un programma che legga un file contenente un numero intero per riga e produca:

  • numero di valori;
  • somma;
  • media;
  • minimo;
  • massimo.

Gestisci:

  • file inesistente;
  • file vuoto;
  • righe non numeriche.

Esercizio 3 --- Filtro

Dato un file di testo, crea:

filtrato.txt

contenente solo le righe che includono una parola scelta dall'utente.


Esercizio 4 --- CSV

Crea un CSV con:

nome,cognome,voto

Scrivi almeno cinque studenti.

Poi rileggi il file con DictReader e calcola la media.


Esercizio 5 --- JSON

Crea un file JSON contenente una lista di prodotti:

[
    {
        "codice": "P01",
        "nome": "Mouse",
        "prezzo": 25.90
    },
    ...
]

Scrivi un programma che:

  • carichi il file;
  • visualizzi i prodotti;
  • calcoli il valore totale;
  • trovi il prodotto più costoso.

Esercizio 6 --- pathlib

Crea 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.


7.66 Sfida finale --- Registro studenti persistente

Realizza una piccola applicazione per gestire un registro studenti.

Il programma deve utilizzare JSON come sistema di persistenza.

Modello dati

Ogni studente contiene:

{
    "id": 1,
    "nome": "Anna",
    "cognome": "Rossi",
    "voti": [8, 7, 9]
}

Requisiti

1. Caricamento

All'avvio:

carica_studenti()

deve leggere:

studenti.json

Se il file non esiste, deve restituire una lista vuota.


2. Salvataggio

Definisci:

salva_studenti(studenti)

che salvi la lista nel file JSON.


3. Inserimento

Definisci:

aggiungi_studente(studenti, studente)

L'ID deve essere univoco.


4. Ricerca

Definisci:

cerca_studente(studenti, testo)

La ricerca deve considerare:

  • nome;
  • cognome.

La ricerca non deve distinguere maiuscole e minuscole.


5. Media

Definisci:

media_studente(studente)

che restituisca la media dei voti.


6. Classifica

Definisci:

classifica(studenti)

che restituisca gli studenti ordinati per media decrescente.


7. Gestione degli errori

Definisci eccezioni personalizzate:

class RegistroError(Exception):
    pass


class StudenteNonTrovatoError(RegistroError):
    pass


class StudenteGiaPresenteError(RegistroError):
    pass

Utilizzale dove appropriato.


8. Menu

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

7.67 Soluzione guidata della sfida

Una possibile organizzazione è:

registro.py
studenti.json

Eccezioni

class RegistroError(Exception):
    pass


class StudenteNonTrovatoError(RegistroError):
    pass


class StudenteGiaPresenteError(RegistroError):
    pass

Caricamento

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 []

Salvataggio

def salva_studenti(studenti):
    with open(
        FILE,
        "w",
        encoding="utf-8"
    ) as file:
        json.dump(
            studenti,
            file,
            ensure_ascii=False,
            indent=4
        )

Ricerca per ID

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"
    )

Inserimento

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.


Ricerca testuale

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()
    ]

Media

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

Classifica

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.


7.68 Un'ulteriore evoluzione: separare il modello dalla persistenza

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.


7.69 Dal file al database

I file sono ottimi per:

  • piccoli progetti;
  • configurazioni;
  • esportazioni;
  • dati locali;
  • esercizi;
  • prototipi.

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.


7.70 Riepilogo del capitolo

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:
    ...

7.71 Regole d'oro del File I/O

1. Preferisci with

with open(...) as file:
    ...

2. Specifica l'encoding per il testo

encoding="utf-8"

3. Ricorda la differenza tra w e a

w → sostituisce
a → aggiunge

4. Non caricare inutilmente file enormi in memoria

Preferisci:

for riga in file:
    ...

quando puoi elaborare i dati progressivamente.

5. Usa pathlib per i percorsi

path = Path("data") / "studenti.json"

6. Usa i moduli standard per i formati strutturati

CSV:

csv.reader()
csv.DictReader()
csv.writer()
csv.DictWriter()

JSON:

json.load()
json.dump()

7. Gestisci gli errori nel punto appropriato

try:
    ...
except FileNotFoundError:
    ...

8. Separa la persistenza dalla logica applicativa

Una classe o funzione che salva i dati non dovrebbe necessariamente conoscere tutte le regole del dominio.


7.72 Verso il capitolo successivo: programmazione funzionale

Nel prossimo capitolo cambieremo prospettiva.

Finora abbiamo utilizzato:

  • variabili;
  • strutture dati;
  • funzioni;
  • classi;
  • eccezioni;
  • file.

La programmazione funzionale ci permetterà di ragionare sempre più in termini di:

dati → trasformazione → risultato

Approfondiremo:

  • funzioni come oggetti;
  • funzioni pure;
  • immutabilità;
  • funzioni di ordine superiore;
  • lambda;
  • map();
  • filter();
  • reduce();
  • sorted() con key;
  • zip();
  • comprehension;
  • generatori;
  • iteratori;
  • closure;
  • decoratori;
  • composizione di funzioni.

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.

← → per navigare F fullscreen