28. L’istruzione with e i context manager
Nel Capitolo 27, hai già usato l’istruzione with per lavorare con i file. Ti ha aiutato a leggere e scrivere dati senza preoccuparti di chiudere esplicitamente il file in seguito. A quel punto, tuttavia, l’attenzione era su come usare with, non su che cosa significhi davvero.
In questo capitolo, facciamo un passo indietro e guardiamo il quadro generale. Imparerai che cosa sono i context manager (context managers), perché gestire le risorse manualmente può essere rischioso e come l’istruzione with fornisca un modello sicuro e affidabile per gestire le risorse in Python. Vedrai anche che with non è limitato ai file e acquisirai una comprensione concettuale di come funziona dietro le quinte.
28.1) Che cosa sono i context manager a livello concettuale
Un context manager (context manager) è un oggetto che definisce cosa dovrebbe accadere quando entri ed esci da un particolare contesto nel tuo codice. Pensa a questo come all’entrare e uscire da una stanza: quando entri, accendi le luci; quando esci, le spegni—qualunque cosa accada mentre sei dentro.
28.1.1) Il problema della gestione delle risorse
Molte attività di programmazione prevedono l’acquisizione di una risorsa, il suo utilizzo e poi il suo rilascio:
# Aprire un file acquisisce una risorsa (file handle)
file = open("data.txt", "r")
content = file.read()
# Usare il file...
file.close() # Rilascio della risorsaQuesto schema compare frequentemente:
- Apertura e chiusura dei file
- Acquisizione e rilascio dei lock nella programmazione concorrente
- Apertura e chiusura delle connessioni al database
- Allocazione e deallocazione dei buffer di memoria
La sfida è garantire che la risorsa venga sempre rilasciata, anche quando qualcosa va storto.
28.1.2) Che cosa rende un oggetto un context manager
Un context manager è qualunque oggetto che implementa due metodi speciali:
__enter__(): chiamato quando si entra nel contesto (all’inizio del bloccowith)__exit__(): chiamato quando si esce dal contesto (alla fine del bloccowith, anche se si verifica un errore)
Non devi implementare questi metodi tu stesso per usare i context manager—i tipi integrati di Python come gli oggetti file li hanno già. Comprendere questo concetto ti aiuta a riconoscere quando stai lavorando con un context manager.
# Gli oggetti file sono context manager
# Hanno i metodi __enter__ e __exit__
file = open("example.txt", "r")
print(hasattr(file, "__enter__")) # Output: True
print(hasattr(file, "__exit__")) # Output: True
file.close()28.1.3) Lo schema di base: setup, uso, teardown
I context manager seguono uno schema in tre fasi:
Fase di setup: acquisire la risorsa (ad es. aprire un file, connettersi a un database, acquisire un lock)
Fase di utilizzo: lavorare con la risorsa (ad es. leggere/scrivere un file, interrogare un database, accedere a dati condivisi)
Fase di teardown: rilasciare la risorsa (ad es. chiudere un file, disconnettersi da un database, rilasciare un lock)
L’intuizione chiave: la fase di teardown avviene sempre, indipendentemente da ciò che accade durante la fase di utilizzo.
28.2) Perché la gestione manuale delle risorse è rischiosa
Prima di imparare l’istruzione with, capiamo perché la gestione manuale delle risorse può fallire e causare problemi.
28.2.1) La chiusura dimenticata
L’errore più comune è semplicemente dimenticare di chiudere una risorsa:
# Lettura di un file di configurazione
config_file = open("config.txt", "r")
settings = config_file.read()
# Ops! Hai dimenticato di chiudere il file
# Il file handle resta apertoSebbene Python alla fine chiuda i file quando il programma termina, lasciare i file aperti può causare problemi:
- Esaurimento delle risorse: i sistemi operativi limitano il numero di file aperti
- Blocco dei file: altri programmi potrebbero non riuscire ad accedere al file
- Perdita di dati: le scritture bufferizzate potrebbero non essere scaricate su disco
28.2.2) Gli errori impediscono la pulizia
Anche quando ti ricordi di chiudere le risorse, gli errori possono impedire che il codice di pulizia venga eseguito:
# Tentativo di elaborare un file
data_file = open("data.txt", "r")
content = data_file.read()
result = process_data(content) # E se questo solleva un errore?
data_file.close() # Questa riga non viene mai eseguita se process_data() fallisce!Se process_data() solleva un’eccezione, il programma salta direttamente alla gestione degli errori, saltando la chiamata a close(). Il file resta aperto indefinitamente.
28.2.3) Punti di uscita multipli
Le funzioni con più istruzioni return rendono la pulizia ancora più difficile:
def read_first_valid_line(filename):
file = open(filename, "r")
for line in file:
line = line.strip()
if line and not line.startswith("#"):
# Trovata una riga valida - ma il file è ancora aperto!
return line
file.close() # Raggiunto solo se non viene trovata alcuna riga valida
return NoneLa funzione ritorna in anticipo quando trova una riga valida, lasciando il file aperto. Dovresti aggiungere file.close() prima di ogni istruzione return—facile da dimenticare e difficile da mantenere.
28.2.4) Gestione complessa degli errori
Potresti provare a usare try-except-finally per garantire la pulizia:
# Tentativo di gestire correttamente gli errori
file = None
try:
file = open("data.txt", "r")
content = file.read()
result = process_data(content)
except FileNotFoundError:
print("File not found")
except ValueError:
print("Invalid data format")
finally:
if file is not None:
file.close()Funziona, ma è prolisso e soggetto a errori. Devi:
- Inizializzare la variabile prima del blocco try
- Controllare se la risorsa è stata acquisita con successo prima di chiuderla
- Ricordarti di includere il blocco finally
- Ripetere questo schema per ogni risorsa
28.2.5) L’impatto nel mondo reale
Questi problemi non sono solo teorici. Considera un programma che elabora migliaia di file:
# ATTENZIONE: perdita di risorse - solo a scopo dimostrativo
# PROBLEMA: i file non vengono mai chiusi
def process_many_files(filenames):
results = []
for filename in filenames:
file = open(filename, "r") # Apre un file
data = file.read()
results.append(analyze(data))
# ERRORE: non chiude mai il file
return results
# Dopo aver elaborato 1000 file, hai 1000 file handle aperti!
# Alla fine, il sistema operativo rifiuta di aprire altri fileOutput (dopo molte iterazioni):
OSError: [Errno 24] Too many open files: 'file_1001.txt'Il programma va in crash perché ha esaurito il limite di file handle del sistema. Questa è una perdita di risorse (resource leak)—le risorse vengono acquisite ma mai rilasciate.
28.3) Usare with oltre i file
L’istruzione with funziona con qualunque context manager, non solo con i file. Esploriamo come risolve i problemi che abbiamo identificato e vediamola usata in vari contesti.
28.3.1) Sintassi di base dell’istruzione with
L’istruzione with ha una struttura semplice:
with expression as variable:
# Blocco di codice che usa la risorsa
# Indentato sotto l’istruzione with
# Risorsa rilasciata automaticamente quiL’expression deve valutarsi in un oggetto context manager. La parte as variable è opzionale ma di solito viene inclusa—ti dà un nome per riferirti alla risorsa.
28.3.2) Usare with per le operazioni sui file
Ecco come l’istruzione with trasforma la gestione dei file:
# Approccio manuale (rischioso)
file = open("data.txt", "r")
content = file.read()
file.close()
# Approccio con istruzione with (sicuro)
with open("data.txt", "r") as file:
content = file.read()
# File chiuso automaticamente qui, anche se si verifica un erroreLa chiusura del file è garantita quando termina il blocco with, sia che il codice completi normalmente sia che sollevi un’eccezione.
28.3.3) Più context manager
Puoi gestire più risorse in una singola istruzione with:
# Lettura da un file e scrittura su un altro
with open("input.txt", "r") as input_file, open("output.txt", "w") as output_file:
for line in input_file:
processed = line.upper()
output_file.write(processed)
# Entrambi i file vengono chiusi automaticamente quiQuesto è equivalente ad annidare istruzioni with ma è più conciso:
# Istruzioni with annidate (equivalenti ma più verbose)
with open("input.txt", "r") as input_file:
with open("output.txt", "w") as output_file:
for line in input_file:
processed = line.upper()
output_file.write(processed)Entrambi gli approcci garantiscono che entrambi i file vengano chiusi correttamente, anche se si verifica un errore durante l’elaborazione.
28.3.4) Lavorare con file compressi
Il modulo gzip di Python fornisce context manager per leggere e scrivere file compressi:
import gzip
# Scrittura di dati compressi
with gzip.open("data.txt.gz", "wt") as compressed_file:
compressed_file.write("This text will be compressed\n")
compressed_file.write("Saving space on disk\n")
# File chiuso automaticamente e compressione finalizzata
# Lettura di dati compressi
with gzip.open("data.txt.gz", "rt") as compressed_file:
content = compressed_file.read()
print(content)Output:
This text will be compressed
Saving space on diskL’istruzione with assicura che il file compresso venga finalizzato correttamente, cosa cruciale per la compressione—una compressione incompleta può risultare in file corrotti.
28.3.5) Cambiare directory temporaneamente
Quando devi cambiare temporaneamente la directory di lavoro corrente, la gestione manuale può essere rischiosa:
import os
# Directory corrente
print(f"Starting in: {os.getcwd()}")
# Cambio manuale di directory (rischioso)
original_dir = os.getcwd()
os.chdir("/tmp")
print(f"Now in: {os.getcwd()}")
process_files() # Se qui si verifica un errore, potremmo non tornare a original_dir
os.chdir(original_dir)Se process_files() solleva un’eccezione, il programma non torna mai alla directory originale, causando potenzialmente comportamenti inattesi nel codice successivo.
Python 3.11 ha introdotto contextlib.chdir(), un context manager che garantisce il ritorno alla directory originale:
import os
from contextlib import chdir
print(f"Starting in: {os.getcwd()}")
# Uso del context manager (sicuro)
with chdir("/tmp"):
print(f"Temporarily in: {os.getcwd()}")
process_files() # Anche se questo solleva un errore, torniamo alla directory originale
print(f"Back in: {os.getcwd()}")
# Ritorno automatico alla directory originaleIl cambio di directory viene automaticamente annullato quando termina il blocco with, sia che il codice completi normalmente sia che sollevi un’eccezione.
28.3.6) Lock di thread per la programmazione concorrente
Nella programmazione concorrente (trattata negli argomenti avanzati), i lock sono context manager:
# Esempio concettuale (impareremo threading negli argomenti avanzati)
import threading
lock = threading.Lock()
# Gestione manuale del lock (rischiosa)
lock.acquire()
# Sezione critica - e se si verifica un errore?
lock.release() # Potrebbe non essere eseguito
# Istruzione with (sicura)
with lock:
# Sezione critica
# Lock rilasciato automaticamente, anche se si verifica un errore
pass28.4) L’istruzione with sotto il cofano (solo concettuale)
Capire come funziona internamente l’istruzione with ti aiuta ad apprezzarne la potenza e a riconoscere quando stai lavorando con i context manager. Questa sezione fornisce una panoramica concettuale—non devi implementare questi dettagli tu stesso.
28.4.1) I due metodi speciali
Ogni context manager implementa due metodi speciali che Python chiama automaticamente:
__enter__(self): chiamato quando inizia il blocco with
- Esegue operazioni di setup (apertura file, acquisizione lock, ecc.)
- Ritorna l’oggetto risorsa che viene assegnato alla variabile dopo
as - Se non è presente alcuna clausola
as, il valore di ritorno viene ignorato
__exit__(self, exc_type, exc_value, traceback): chiamato quando termina il blocco with
- Esegue operazioni di pulizia (chiusura file, rilascio lock, ecc.)
- Riceve informazioni su qualunque eccezione si sia verificata
- Viene chiamato sempre, anche se è stata sollevata un’eccezione
- Può sopprimere le eccezioni restituendo
True(raramente usato)
28.4.2) Come Python esegue un’istruzione with
Tracciamo cosa succede quando Python esegue un’istruzione with:
with open("data.txt", "r") as file:
content = file.read()
print(content)Ecco l’esecuzione passo dopo passo:
Passo 1: Python valuta open("data.txt", "r"), creando un oggetto file
Passo 2: Python chiama il metodo __enter__() dell’oggetto file
Passo 3: __enter__() restituisce l’oggetto file stesso, che viene assegnato a file
Passo 4: Python esegue il blocco di codice indentato
Passo 5: Quando il blocco termina (normalmente o a causa di un’eccezione), Python chiama __exit__()
Passo 6: __exit__() chiude il file ed esegue la pulizia
Passo 7: Se si è verificata un’eccezione, Python la rilancia dopo la pulizia
28.4.3) Gestione delle eccezioni nei context manager
Quando si verifica un’eccezione all’interno di un blocco with, Python passa a __exit__() informazioni su di essa:
# Cosa succede quando si verifica un errore
try:
with open("data.txt", "r") as file:
content = file.read()
result = int(content) # Might raise ValueError
print(result)
except ValueError as e:
print(f"Invalid data: {e}")
# Il file viene chiuso prima che venga eseguito il blocco exceptFlusso di esecuzione quando si verifica ValueError:
Il punto chiave: __exit__() viene chiamato prima che l’eccezione si propaghi, assicurando che la pulizia avvenga anche quando si verificano errori.
28.4.4) Un semplice modello mentale
Pensa all’istruzione with come a una garanzia:
with resource_manager as resource:
# Usa la risorsa
pass
# Python GARANTISCE che la pulizia sia avvenutaQualunque cosa accada all’interno del blocco—completamento normale, istruzione return, eccezione o persino errori di sistema—Python chiama __exit__() per ripulire. Questa garanzia è ciò che rende with così potente ed è il motivo per cui dovresti usarlo ogni volta che lavori con risorse.
Punti chiave di questo capitolo:
- I context manager (context managers) definiscono operazioni di setup e pulizia per le risorse
- La gestione manuale delle risorse è rischiosa a causa di pulizia dimenticata, errori e punti di uscita multipli
- L’istruzione
withgarantisce che la pulizia avvenga, anche quando si verificano errori - Usa
withper i file e per qualunque altra risorsa che richieda pulizia - Più risorse possono essere gestite in una singola istruzione
with - Sotto il cofano,
withchiama automaticamente i metodi__enter__()e__exit__() __exit__()viene sempre eseguito, assicurando che le risorse vengano rilasciate correttamente
L’istruzione with trasforma la gestione delle risorse da lavoro manuale soggetto a errori in una pulizia automatica e affidabile. Usala ogni volta che lavori con file, connessioni al database, lock o qualunque altra risorsa che richieda una pulizia corretta. Il tuo codice sarà più sicuro, più pulito e più professionale.