La storia - in Python, per gestire le risorse (file, connessioni, transazioni) si utilizza la costruzione with, basata sul protocollo dei gestori di contesto (enter, exit). Per casi semplici, scrivere un'intera classe è superfluo, quindi è stato proposto il decoratore @contextmanager (modulo contextlib), che consente di definire gestori di risorse come generatori.
Problema - liberare o chiudere manualmente le risorse è scomodo, il codice diventa fragile agli errori (ad esempio, si dimentica di chiudere un file). Inoltre, non si vuole scrivere una classe separata con due metodi per semplici operazioni (ad esempio, cambiando temporaneamente directory o stdout).
Soluzione - utilizzare @contextmanager per descrivere in modo conciso l' "inizio" e la "fine" dell'uso della risorsa, gestendo in modo garantito le eccezioni e il rilascio.
Esempio di codice:
from contextlib import contextmanager @contextmanager def open_file(filename, mode): f = open(filename, mode) try: yield f finally: f.close() with open_file('test.txt', 'w') as f: f.write('Hello')
Caratteristiche chiave:
Si può fare in modo che yield da @contextmanager restituisca più oggetti (ad esempio, tramite una tupla)?
Sì, è possibile e anche comodo per restituire un "gruppo" di risorse correlate.
Esempio di codice:
@contextmanager def managed_two(): a, b = [], {} try: yield a, b finally: a.clear(); b.clear()
Cosa succede se dopo yield si genera un'eccezione - la risorsa verrà chiusa?
Sì, il blocco finally verrà eseguito in ogni caso, anche se dal codice all'interno di with si genera un errore/eccezione.
Può @contextmanager sostituire una vera e propria classe gestore di contesto con enter/exit?
Nella maggior parte dei casi banali - sì, per casi più complessi con stati nidificati o ereditarietà è più comodo lavorare tramite una classe.
Aprire e chiudere manualmente un file:
f = open('test.txt', 'w') try: f.write('Hello') finally: f.close()
Pro:
Contro:
Utilizzare @contextmanager per cambiare temporaneamente la directory di lavoro o aprire un file (o impostazioni ambientali):
@contextmanager def work_in(dirname): import os prev = os.getcwd() os.chdir(dirname) try: yield finally: os.chdir(prev)
Pro:
Contro: