L'istruzione with di Python e i Context Manager
Scopri come funziona l'istruzione with di Python e i context manager, come scriverne di propri con __enter__/__exit__ e come usare contextlib.
L'istruzione with garantisce che le risorse come file, connessioni di rete e lock vengano impostate e rilasciate correttamente — anche quando un'eccezione interrompe il blocco. L'oggetto che controlla questa configurazione e il relativo smontaggio si chiama context manager.
Questo capitolo spiega come funziona l'istruzione with, quando usarla, come scrivere i propri context manager usando __enter__ e __exit__, e come crearne di leggeri con contextlib.contextmanager.
Perché esiste with
Prima dell'istruzione with, la gestione delle risorse richiedeva di scrivere blocchi try/finally a mano:
f = open("data.txt", "r", encoding="utf-8")
try:
content = f.read()
finally:
f.close() # must always close, even if read() raisesQuesto funziona, ma è verboso, facile da dimenticare e aggiunge codice ripetitivo attorno a ogni risorsa. L'istruzione with condensa tutto in un singolo blocco leggibile e gestisce la pulizia automaticamente:
with open("data.txt", "r", encoding="utf-8") as f:
content = f.read()
# f is closed here, no matter what happened inside the blockLa clausola as f lega il valore del context manager al nome f. Alcuni context manager non producono un valore utile; in tal caso è possibile omettere as:
with some_lock:
shared_data.append(item)Come funziona l'istruzione with
Quando Python esegue un'istruzione with, segue questa sequenza:
- Valuta l'espressione dopo
with— questa produce l'oggetto context manager. - Chiama il metodo
__enter__()del context manager. Il valore restituito da__enter__()viene assegnato alla variabileas(se presente). - Esegue il corpo del blocco
with. - Chiama il metodo
__exit__(exc_type, exc_val, exc_tb)del context manager.- Se il blocco è terminato normalmente, tutti e tre gli argomenti sono
None. - Se è stata sollevata un'eccezione, i tre argomenti la descrivono.
- Se
__exit__restituisce un valore truthy, l'eccezione viene soppressa e l'esecuzione continua dopo il bloccowith. Se restituisce un valore falsy (oNone), l'eccezione si propaga.
- Se il blocco è terminato normalmente, tutti e tre gli argomenti sono
Questo protocollo si chiama protocollo del context manager.
Apertura di file con with
L'uso più comune di with è la gestione dei file. Gli oggetti file built-in di Python implementano il protocollo del context manager, quindi si chiudono automaticamente al termine del blocco:
with open("report.txt", "w", encoding="utf-8") as f:
f.write("Sales: 1 000\n")
f.write("Returns: 23\n")
print(f.closed) # True — file was closed on exitSe si verifica un'eccezione all'interno del blocco, il file viene comunque chiuso:
try:
with open("data.txt", "r", encoding="utf-8") as f:
raise RuntimeError("something went wrong")
except RuntimeError:
pass
print(f.closed) # True — closed despite the exceptionSenza with, dimenticare f.close() dopo un errore lascia il file descriptor aperto fino all'esecuzione del garbage collector — o fino all'uscita del processo — il che può causare perdita di dati o errori "too many open files" nei programmi a lunga esecuzione.
Apertura di più risorse contemporaneamente
È possibile aprire più risorse in un'unica istruzione with separandole con virgole (Python 3.1+):
with open("input.txt", "r", encoding="utf-8") as src, \
open("output.txt", "w", encoding="utf-8") as dst:
for line in src:
dst.write(line.upper())Questo equivale esattamente ad annidare due istruzioni with, ma mantiene il livello di indentazione piatto.
Scrivere un context manager con __enter__ e __exit__
Qualsiasi classe che definisce __enter__ e __exit__ può essere usata con l'istruzione with. Ecco un esempio minimale — un timer che misura quanto dura il blocco with:
import time
class Timer:
def __enter__(self):
self._start = time.perf_counter()
return self # bound to the 'as' variable
def __exit__(self, exc_type, exc_val, exc_tb):
elapsed = time.perf_counter() - self._start
print(f"Elapsed: {elapsed:.4f}s")
return False # do not suppress exceptions
with Timer() as t:
total = sum(range(1_000_000))
# Elapsed: 0.0xxx s
print(total) # 499999500000Punti chiave:
__enter__viene eseguito prima del blocco. Restituisce il valore assegnato aas t. Restituireselfconsente al chiamante di accedere at.elapsede ad altri attributi se necessario.__exit__viene eseguito dopo il blocco, anche in caso di eccezione. RestituireFalse(oNone) lascia propagare normalmente qualsiasi eccezione.
Sopprimere eccezioni in __exit__
Se __exit__ restituisce True, l'eccezione viene ingoiata e l'esecuzione continua dopo il blocco with. Questo è intenzionale in contesti specifici — ad esempio, un context manager che cattura e registra gli errori senza far crashare il programma:
class Ignore:
"""Silently ignore any exception raised inside the with block."""
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type is not None:
print(f"Suppressed: {exc_type.__name__}: {exc_val}")
return True # suppress the exception
with Ignore():
x = 1 / 0 # ZeroDivisionError is caught and ignored
print("execution continues here")
# Suppressed: ZeroDivisionError: division by zero
# execution continues hereUsa la soppressione delle eccezioni con cautela — ingoiare silenziosamente gli errori può nascondere bug. contextlib.suppress della libreria standard è il modo idiomatico per farlo (vedi sotto).
Un context manager per connessioni al database
Un esempio più realistico — gestire una connessione di tipo database che esegue il commit in caso di successo e il rollback in caso di errore:
class ManagedTransaction:
def __init__(self, connection):
self.conn = connection
def __enter__(self):
self.conn.begin()
return self.conn
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type is None:
self.conn.commit()
else:
self.conn.rollback()
return False # always let exceptions propagateIl pattern — commit in caso di successo, rollback in caso di errore — compare in tutte le librerie di database reali (SQLite, SQLAlchemy, psycopg2 lo implementano tutte).
contextlib.contextmanager: Context Manager basati su generatori
Scrivere una classe completa con __enter__ e __exit__ è l'approccio corretto per context manager complessi o con stato. Per i casi più semplici, il decoratore contextlib.contextmanager permette di esprimere la stessa logica come funzione generatrice:
from contextlib import contextmanager
@contextmanager
def managed_open(path, mode="r", encoding="utf-8"):
print(f"Opening {path}")
f = open(path, mode, encoding=encoding)
try:
yield f # everything up to yield is __enter__
finally:
f.close() # everything after yield is __exit__
print(f"Closed {path}")
with managed_open("notes.txt", "w") as f:
f.write("hello\n")
# Opening notes.txt
# Closed notes.txtIl protocollo del generatore si mappa direttamente sul protocollo del context manager:
- Codice prima di
yield→__enter__(setup). - L'espressione
yield→ il valore assegnato alla variabileas. - Codice dopo
yield(di solito in unfinally) →__exit__(teardown).
Il try/finally attorno a yield è importante: senza di esso, un'eccezione all'interno del blocco with causerebbe il mancato eseguimento del codice di teardown.
Esempio con contextmanager: directory di lavoro temporanea
import os
from contextlib import contextmanager
@contextmanager
def working_directory(path):
original = os.getcwd()
os.chdir(path)
try:
yield
finally:
os.chdir(original)
with working_directory("/tmp"):
print(os.getcwd()) # /tmp (or system temp dir)
print(os.getcwd()) # restored to original directoryQuesto pattern è disponibile anche nella libreria standard come tempfile.TemporaryDirectory.
Utilità di contextlib
Il modulo contextlib include diversi context manager pronti all'uso che vale la pena conoscere:
contextlib.suppress
Sopprimi eccezioni specifiche senza alcun codice ripetitivo:
from contextlib import suppress
with suppress(FileNotFoundError):
os.remove("temp.txt") # no error even if file does not existEquivalente a un try/except che non fa nulla sull'eccezione catturata.
contextlib.nullcontext
Un context manager no-op utile quando si vuole condizionalmente usare un context manager oppure no:
from contextlib import nullcontext
def process(data, lock=None):
ctx = lock if lock is not None else nullcontext()
with ctx:
return sorted(data)Senza nullcontext, sarrebbe necessario un ramo if lock: ogni volta.
contextlib.ExitStack
ExitStack permette di gestire un numero dinamico di context manager — utile quando il numero di risorse non è noto fino al runtime:
from contextlib import ExitStack
files = ["a.txt", "b.txt", "c.txt"]
with ExitStack() as stack:
handles = [
stack.enter_context(open(f, "w", encoding="utf-8"))
for f in files
]
for i, fh in enumerate(handles):
fh.write(f"file {i}\n")
# All three files are closed hereExitStack è anche lo strumento giusto quando è necessario aggiungere condizionalmente un context manager, o quando si vuole rimandare la pulizia a un momento successivo.
Quando usare with vs. Try/Finally
Usa with ogni volta che:
- Una risorsa deve essere rilasciata dopo l'uso (file, socket, lock, cursori di database).
- Vuoi garantire la pulizia anche in caso di eccezioni.
- La logica di pulizia è sempre la stessa indipendentemente dal successo o dal fallimento.
Usa un semplice try/finally solo quando:
- Hai bisogno di azioni di pulizia diverse a seconda del tipo di eccezione — anche se
__exit__può farlo ugualmente. - Stai scrivendo codice compatibile con Python 2 (raro oggigiorno).
In pratica, se l'oggetto supporta il protocollo del context manager, preferisci sempre with.
Riferimento rapido
| Funzionalità | Cosa fa |
|---|---|
with expr as v: | Chiama expr.__enter__(), assegna il risultato a v, chiama __exit__ all'uscita |
| Risorse multiple | with A() as a, B() as b: — entrambe pulite anche se B() solleva un'eccezione |
__enter__(self) | Setup; il valore restituito è assegnato alla variabile as |
__exit__(self, exc_type, exc_val, exc_tb) | Teardown; restituisce True per sopprimere l'eccezione |
@contextmanager | Trasforma una funzione generatrice in un context manager |
contextlib.suppress(E) | Inghiotte il tipo di eccezione E senza un try/except |
contextlib.nullcontext() | Segnaposto quando un context manager è facoltativo |
contextlib.ExitStack | Gestisce un insieme dinamico o condizionale di context manager |
Capitoli correlati
- Gestione dei file Python — usare
with open(...)per ogni operazione su file - Python Try Except — gestione delle eccezioni e try/finally
- Decoratori Python —
@contextmanagerusa lo stesso pattern dei decoratori - Generatori Python — come il protocollo dei generatori alimenta
@contextmanager - Classi e oggetti Python — scrittura dei metodi
__enter__e__exit__