Lezioni del modulo (3/4)
Context manager: with e __enter__/__exit__
Un context manager è un oggetto che usi con with. Garantisce che certe
operazioni di setup/cleanup avvengano sempre, anche in presenza di errori.
L'esempio canonico: aprire e chiudere un file.
Il pattern with
with open("note.txt", "w") as f:
f.write("ciao")
# qui f è già chiuso, anche se write() ha sollevato un'eccezioneTradotto in italiano: "con la risorsa X aperta come f, fai questo. Al
termine (qualunque cosa accada), chiudi X." Senza with dovresti scrivere
un try/finally ogni volta.
Più di uno alla volta
with open("a.txt") as f, open("b.txt") as g:
dati_a = f.read()
dati_b = g.read()Scrivere il proprio context manager (classe)
Serve definire due metodi dunder: __enter__ e __exit__.
class Timer:
def __enter__(self):
import time
self.t0 = time.perf_counter()
return self # è l'oggetto che finisce dopo "as"
def __exit__(self, exc_type, exc_value, traceback):
import time
self.elapsed = time.perf_counter() - self.t0
# ritornare True ingoia l'eccezione; di solito non lo vuoi
return False
with Timer() as t:
sum(range(10**6))
t.elapsed # quanto è durato__enter__(self)viene chiamato all'ingresso del blocco; il valore ritornato è ciò che ottieni dopoas.__exit__(self, exc_type, exc_value, tb)viene chiamato all'uscita (sempre). Se ritornaTrue, eventuali eccezioni vengono soppresse.
La via breve: @contextmanager
contextlib.contextmanager ti permette di scrivere un context manager come
funzione generatore:
from contextlib import contextmanager
@contextmanager
def timer():
import time
t0 = time.perf_counter()
try:
yield # qui sta il blocco "with"
finally:
elapsed = time.perf_counter() - t0
print(f"durata: {elapsed:.4f}s")
with timer():
sum(range(10**6))Il codice prima dello yield è __enter__, quello dopo è __exit__.
Il try/finally garantisce il cleanup anche in caso di errori.
suppress: ignora un'eccezione
from contextlib import suppress
with suppress(FileNotFoundError):
open("forse-non-esiste.txt").read()
# se il file non c'è, nessun errore propagaPitfall: with solo per oggetti che lo supportano
x = 42
with x: # AttributeError: __enter__
...Scrivere context manager con @contextmanager
Il modulo contextlib della libreria standard offre un decoratore estremamente comodo chiamato @contextmanager per definire context manager senza dover scrivere intere classi con i metodi __enter__ e __exit__. Basta definire una funzione generatore con un'istruzione yield:
from contextlib import contextmanager
@contextmanager
def gestisci_risorsa():
# codice __enter__
yield risorsa
# codice __exit__Prova tu
Usa contextlib.suppress per provare a fare int('non-numero') ignorando ValueError. Dopo il blocco assegna `ok = True`. Valuta `ok`.
Mostra suggerimento
suppress(ValueError) ingoia solo ValueError.
Soluzione disponibile dopo 3 tentativi
Esercizio di ripasso
Scrivi un context manager `collect` con @contextmanager che faccia yield di una lista vuota e, all'uscita, la converta in tupla (stampala). Usalo per appendere 1, 2, 3 alla lista nel blocco. Alla fine, valuta `len(collection)` dove `collection` è la lista yieldata.
Mostra suggerimento
yield items per esporre la lista nel blocco with.
Soluzione disponibile dopo 3 tentativi
Sfida aggiuntiva
Scrivi un context manager personalizzato usando il decoratore `@contextmanager` da `contextlib`. Il context manager deve chiamarsi `mock_session`. All'ingresso deve inserire `'start'` in una lista globale `log_list = []`, cedere il controllo con `yield`, e all'uscita appendere `'stop'`. Valuta `log_list` dopo aver usato il context manager con `with mock_session():`.
Mostra suggerimento
Usa yield all'interno di mock_session per cedere il controllo prima di aggiungere 'stop'.
Soluzione disponibile dopo 3 tentativi