Historia — w Pythonie do zarządzania zasobami (pliki, połączenia, transakcje) używa się konstrukcji with, opartej na protokole menedżerów kontekstu (enter, exit). W prostych przypadkach pisanie całej klasy jest zbędne, dlatego zaproponowano dekorator @contextmanager (moduł contextlib), który pozwala definiować menedżerów zasobów jako generatory.
Problem — ręczne zwalnianie lub zamykanie zasobów jest niewygodne, a kod staje się podatny na błędy (na przykład zapomnienie o zamknięciu pliku). Również nie chcemy, aby dla prostych rzeczy (na przykład tymczasowej zmiany katalogu lub stdout) pisać osobnej klasy z dwiema metodami.
Rozwiązanie — użyć @contextmanager, aby zwięźle opisać "początek" i "koniec" korzystania z zasobu, zapewniając jednocześnie obsługę wyjątków i zwolnienie zasobów.
Przykład kodu:
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')
Kluczowe cechy:
Czy można tak skonstruować, aby w yield z @contextmanager zwracano kilka obiektów (na przykład przez krotkę)?
Tak, można i jest to wygodne do zwracania "grupy" powiązanych zasobów.
Przykład kodu:
@contextmanager def managed_two(): a, b = [], {} try: yield a, b finally: a.clear(); b.clear()
Co się stanie, jeśli po yield zostanie rzucony wyjątek — czy zasób zostanie zamknięty?
Tak, blok finally zostanie wykonany w każdym przypadku, nawet jeśli w kodzie wewnątrz with wystąpi błąd/wyjątek.
Czy @contextmanager może zastąpić pełnoprawną klasę menedżera kontekstu z enter/exit?
W większości trywialnych przypadków — tak, w bardziej złożonych z zagnieżdżonymi stanami lub dziedziczeniem łatwiej pracować za pomocą klasy.
Ręczne otwieranie i zamykanie pliku:
f = open('test.txt', 'w') try: f.write('Hello') finally: f.close()
Zalety:
Wady:
Użycie @contextmanager do tymczasowej zmiany katalogu roboczego lub otwarcia pliku (lub konfiguracji środowiska):
@contextmanager def work_in(dirname): import os prev = os.getcwd() os.chdir(dirname) try: yield finally: os.chdir(prev)
Zalety:
Wady: