Python & AI Tutorials Logo
Programmazione Python

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:

python
# Aprire un file acquisisce una risorsa (file handle)
file = open("data.txt", "r")
content = file.read()
# Usare il file...
file.close()  # Rilascio della risorsa

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

  1. __enter__(): chiamato quando si entra nel contesto (all’inizio del blocco with)
  2. __exit__(): chiamato quando si esce dal contesto (alla fine del blocco with, 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.

python
# 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:

Entra nel contesto

Setup: chiamato enter

Usa la risorsa

Esci dal contesto

Teardown: chiamato exit

Risorsa rilasciata

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:

python
# 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 aperto

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

python
# 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:

python
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 None

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

python
# 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:

python
# 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 file

Output (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:

python
with expression as variable:
    # Blocco di codice che usa la risorsa
    # Indentato sotto l’istruzione with
# Risorsa rilasciata automaticamente qui

L’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:

python
# 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 errore

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

python
# 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 qui

Questo è equivalente ad annidare istruzioni with ma è più conciso:

python
# 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:

python
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 disk

L’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:

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

python
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 originale

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

python
# 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
    pass

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

python
with open("data.txt", "r") as file:
    content = file.read()
    print(content)

Ecco l’esecuzione passo dopo passo:

Oggetto fileInterprete PythonIl tuo codiceOggetto fileInterprete PythonIl tuo codiceEsegui istruzione withChiama __enter__()Restituisci oggetto fileAssegna alla variabile 'file'Chiama file.read()Restituisci contenutoStampa contenutoEsci dal blocco withChiama __exit__()Chiudi fileRestituisci NoneContinua esecuzione

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:

python
# 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 except

Flusso di esecuzione quando si verifica ValueError:

Entra nel blocco with

Chiama enter

Esegui: content = file.read

Esegui: result = int content

ValueError sollevata

Chiama exit con info sull’eccezione

Chiudi file

Rilancia ValueError

Il blocco except la intercetta

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:

python
with resource_manager as resource:
    # Usa la risorsa
    pass
# Python GARANTISCE che la pulizia sia avvenuta

Qualunque 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 with garantisce che la pulizia avvenga, anche quando si verificano errori
  • Usa with per i file e per qualunque altra risorsa che richieda pulizia
  • Più risorse possono essere gestite in una singola istruzione with
  • Sotto il cofano, with chiama 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.

© 2025. Primesoft Co., Ltd.
support@primesoft.ai