Menu

Context manager in Python: l'istruzione with spiegata

Cosa fa davvero l'istruzione with: pulizia automatica per file, lock, connessioni al database e qualsiasi altra cosa che vada chiusa in modo affidabile.

Questa pagina include editor eseguibili: modifica, esegui e vedi subito l'output.

L'istruzione che fa pulizia da sola

Ogni risorsa che apri in un programma (un file, una connessione di rete, un handle di database, un lock) va chiusa quando hai finito. Se te ne dimentichi, sprechi memoria, tieni lock che bloccano altri processi o corrompi file in caso di crash. L'istruzione with di Python se ne occupa al posto tuo.

Lo schema da cui partono tutti è la lettura di un file:

with open("notes.txt") as f:
    contents = f.read()
    print(contents)

Due cose succedono in automatico. All'ingresso, open() ti dà un oggetto file associato a f. All'uscita, che il blocco finisca normalmente, esca prima con return o sollevi un'eccezione, Python chiama f.close() per te.

Tutto qui. Questo è l'intero scopo di with.

Cosa sostituisce "with"

Prima dei context manager, il codice sicuro equivalente era un try/finally:

f = open("notes.txt")
try:
    contents = f.read()
    print(contents)
finally:
    f.close()

Cinque righe di cerimonia per "leggi un file e chiudilo quando hai finito". Moltiplicale per ogni open in un programma più grande e capisci perché piace. with è più breve, più difficile da sbagliare, e con lui è impossibile dimenticare la pulizia.

Aprire più risorse

Puoi associare più context manager in un solo with:

with open("input.txt") as src, open("output.txt", "w") as dst:
    dst.write(src.read().upper())

Entrambi i file si aprono all'ingresso ed entrambi si chiudono all'uscita. Se il primo open riesce e il secondo solleva un'eccezione, Python chiude comunque il primo: il meccanismo gestisce correttamente anche una preparazione parziale.

Per liste di risorse più lunghe, la forma con le parentesi (Python 3.10+) è più chiara:

with (
    open("a.txt") as a,
    open("b.txt") as b,
    open("c.txt") as c,
):
    ...

Cos'è davvero un context manager

Qualsiasi oggetto che definisce __enter__ e __exit__ è un context manager. Il protocollo è semplicissimo:

  • __enter__(self) viene eseguito quando inizia il blocco with. Il suo valore di ritorno è quello che viene associato a as name.
  • __exit__(self, exc_type, exc_value, traceback) viene eseguito quando il blocco finisce, in qualunque modo. Se l'uscita è causata da un'eccezione, le informazioni sull'eccezione vengono passate in modo che il context manager possa esaminarla o sopprimerla.

Eccone uno minimo che misura il tempo del blocco che avvolge:

with Timer(): crea l'oggetto, chiama il suo __enter__, esegue il corpo e chiama __exit__. Niente file, niente lock: solo un piccolo involucro attorno a "fai qualcosa e misura quanto ci hai messo".

La scorciatoia contextlib.contextmanager

Definire una classe per ogni context manager è più pesante del necessario. contextlib.contextmanager trasforma una funzione generatore in un context manager: un solo yield separa il "prima" dal "dopo":

Tutto ciò che sta prima di yield è il comportamento di __enter__. Tutto ciò che sta dopo è __exit__. Il try/finally fa sì che la pulizia venga eseguita anche se il corpo solleva un'eccezione.

La maggior parte dei context manager personalizzati che scriverai ha questa forma. Parti dalla forma con il decoratore; passa a una classe solo quando ti serve qualcosa che la forma a generatore non può esprimere.

Cambiare qualcosa temporaneamente

Uno schema comune: imposta qualcosa, usalo, ripristinalo. I context manager lo esprimono in modo pulito:

Qualsiasi schema "imposta e poi ripristina" (variabili d'ambiente, livello di dettaglio del logging, feature flag, fixture dei test) si adatta in modo naturale a un context manager. Chi lo usa non deve ricordarsi di ripristinare niente.

Sopprimere le eccezioni

Il metodo __exit__ può restituire True per dire a Python "ho gestito io l'eccezione, ignorala". È raro e di solito è un campanello d'allarme, ma è così che funziona contextlib.suppress:

suppress(FileNotFoundError) trasforma il FileNotFoundError in un'operazione nulla. Usalo per operazioni davvero facoltative: "prova questo, non importa se non funziona". Non usarlo per zittire eccezioni a cui non hai pensato.

Altri context manager che incontrerai

Appena inizi a farci caso, i context manager compaiono ovunque nella libreria standard:

import threading
from pathlib import Path

# Lock: garantiscono il rilascio anche se la sezione critica solleva un'eccezione.
lock = threading.Lock()
with lock:
    ...

# tempfile: cancella il file temporaneo quando hai finito.
from tempfile import TemporaryDirectory
with TemporaryDirectory() as tmp:
    path = Path(tmp) / "scratch.txt"
    path.write_text("hello")

# Connessioni al database: chiude la connessione (o termina la transazione).
import sqlite3
with sqlite3.connect(":memory:") as conn:
    conn.execute("CREATE TABLE t (x INTEGER)")

Le librerie di terze parti seguono le stesse convenzioni. Quando vedi with something as x:, quasi sempre significa "usa x per la durata di questo blocco e poi fai pulizia".

Quando non usare with

  • Quando non hai davvero una fase di preparazione e una di chiusura. Avvolgere codice qualsiasi in un context manager senza motivo aggiunge solo rumore.
  • Quando ti serve la risorsa in tanti blocchi scollegati. Tenere aperto un with per tutta la vita di uno script lungo può nascondere qual è davvero l'ambito della pulizia. Valuta invece una classe che possieda la risorsa.
  • Quando un decoratore si adatta meglio. Alcuni schemi ripetuti (riprova, log, misura del tempo) si leggono meglio come @decorator su una funzione che come with ...: al suo interno. Scegli quello che si legge meglio nel punto della chiamata.

Nella maggior parte dei casi with è la scelta giusta. Le rare eccezioni sono facili da riconoscere quando sai cosa cercare.

Prossimo passo: lavorare con file reali

Ora conosci il meccanismo dietro with open(...) as f:, che è il contesto in cui lo userai nove volte su dieci. Il prossimo capitolo lo mette al lavoro per leggere, scrivere e muoversi tra i file su disco.

Domande frequenti

Cosa fa with open in Python?

with open(path) as f: apre il file e lo associa a f per tutta la durata del blocco. Quando il blocco finisce, normalmente o a causa di un'eccezione, Python chiude automaticamente il file. Non ti serve f.close(): ci pensa l'istruzione with.

Perché usare with invece del semplice open()?

Perché with chiude il file anche quando un'eccezione scatta a metà del blocco. Con il semplice open() tocca a te ricordarti close() in ogni percorso del codice, compresi quelli di errore. with è più sicuro e più breve.

Come apro più file con una sola istruzione with?

Separa i context manager con una virgola: with open('a.txt') as a, open('b.txt') as b:. Entrambi i file vengono aperti all'ingresso e chiusi all'uscita, in ordine inverso. Sostituisce le istruzioni with annidate quando ti servono più risorse contemporaneamente.

Illustrazione dei linguaggi di programmazione di Coddy

Impara a programmare con Coddy

INIZIA