I context manager permettono a Python di garantire che una risorsa venga pulita, sia che il codice del blocco abbia successo o che fallisca. Un articolo tecnico ne dettaglia il funzionamento con l’istruzione with, e diversi modi per costruirli e utilizzarli, ben oltre la gestione dei file.
Molti sviluppatori scoprono questo meccanismo con with open(path), che chiude automaticamente il file. Secondo l’autore, i file sono però il caso meno interessante: l’istruzione with delimita soprattutto un blocco la cui uscita è garantita. Il protocollo si basa su due metodi, __enter__(), chiamata all’ingresso del blocco, e __exit__(), chiamata all’uscita, che sia normale, causata da un return o dovuta a un’eccezione.
Il valore restituito da __exit__() gioca un ruolo decisivo. Se è vero, l’eccezione viene soppressa, il che può passare inosservato: il programma continua e l’errore scompare senza traccia. Se è False o assente, l’eccezione risale dopo la pulizia. Ecco perché i context manager presentati nell’articolo non restituiscono mai un valore vero.
Costruire i propri context manager
Una classe è adatta quando il context manager deve mantenere uno stato che il chiamante può rileggere. L’articolo prende l’esempio di un timer, che misura la durata di un blocco grazie a time.perf_counter() e mostra il risultato anche se il blocco fallisce. Un’estrazione di caratteristiche interrotta mostra così «feature extraction: 12.418s (failed)», prima che l’errore risalga.
Per un compito lineare, cioè una configurazione, un blocco e poi un ripristino, il decoratore @contextmanager permette di scrivere una semplice funzione generatrice. Tutto ciò che precede il yield viene eseguito all’ingresso, tutto ciò che segue all’uscita. L’articolo raccomanda di mettere il ripristino in un finally, e di non ingoiare mai l’eccezione in un try/except senza rilanciarla. Un solo valore deve essere prodotto dal yield, altrimenti viene sollevato un errore RuntimeError.
Ripristinare lo stato e gestire più risorse
La libreria standard fornisce già diversi context manager. TemporaryDirectory() crea una directory con un nome unico e la elimina alla fine del blocco, evitando così file residui di un’esecuzione precedente. Da Python 3.12, il parametro delete=False permette di conservare i file per un’analisi a posteriori. Su Windows, un file rimasto aperto può far fallire l’eliminazione; il parametro ignore_cleanup_errors=True, disponibile da Python 3.10, attenua questo problema senza risolverlo.
Per le modifiche ai dizionari, patch.dict(), proveniente dal modulo unittest.mock, ripristina lo stato originale dopo il blocco: una chiave esistente riprende il suo valore e una chiave assente viene eliminata. L’articolo sottolinea tuttavia che os.environ è globale al processo. Tutti i task e tutti i thread vedono quindi la modifica, il che la rende rischiosa in codice concorrente.
Quando il numero di risorse è noto solo a runtime, ad esempio il numero di file da unire, ExitStack raggruppa i context manager. Il suo metodo enter_context() ne registra uno, e callback() una semplice funzione. All’uscita, gli elementi vengono rimossi dallo stack in ordine inverso. L’articolo precisa che questa rimozione avviene alla fine del blocco o alla chiamata di close(), e non alla cancellazione dell’oggetto da parte del garbage collector. Una classe dedicata può essere preferibile quando lo stack diventa troppo pesante.
