Menedżery kontekstu umożliwiają automatyzację i kontrolowanie wykonania bloków kodu z gwarantowaną finalizacją zasobów — na przykład automatyczne zamykanie plików, zwalnianie menedżerów transakcji w bazach danych lub blokowanie/odblokowywanie wątków. W standardzie Pythona ta paradygma została wprowadzona do bezpiecznego i wygodnego zarządzania zewnętrznymi zasobami bez konieczności ręcznego śledzenia finalnej fazy pracy.
Historia:
Przed wprowadzeniem menedżerów kontekstu konieczne było jawne wywoływanie metod close()/release() w celu zwolnienia zasobów, co prowadziło do błędów w przypadku wystąpienia wyjątków. Dzięki konstrukcji with ... as ...: Python umożliwił wyraźne definiowanie „zakresu działania zasobu”, automatycznie wywołując metody do inicjalizacji i zakończenia pracy z nim.
Problem:
Ręczne zarządzanie „zamykanie” zasobu jest niebezpieczne — jeśli zapomnisz o close() (lub release()), zasoby pozostaną zajęte aż do zakończenia procesu lub mogą nawet zawiesić się na zawsze. Jest to szczególnie dotkliwe w przypadku pracy z plikami, połączeniami sieciowymi, transakcjami w bazach danych.
Rozwiązanie:
Menedżery kontekstu są realizowane za pomocą magicznych metod enter() i exit(). Przy wejściu do bloku with Python wywołuje enter, a przy wyjściu — exit, nawet jeśli wewnątrz bloku wystąpił wyjątek.
Przykład kodu:
class ManagedFile: def __init__(self, filename): self.filename = filename self.file = None def __enter__(self): self.file = open(self.filename, 'w') return self.file def __exit__(self, exc_type, exc_val, exc_tb): if self.file: self.file.close() # Można stłumić wyjątek return True, zazwyczaj return False with ManagedFile('demo.txt') as f: f.write('Hello, world! ') # plik jest gwarantowane zamknięty
Kluczowe cechy:
Czy można używać tego samego egzemplarza menedżera kontekstu kilka razy z rzędu w różnych with?
Nie można: zazwyczaj zasób jest otwierany w enter, zwalniany w exit, a obiekt staje się niepoprawny do ponownego używania. Lepiej jest tworzyć nowy obiekt dla każdego bloku with.
Co zrobić, jeśli trzeba używać kilku zasobów w jednym with?
Można rozdzielić zmienne przecinkiem:
with open('a.txt') as fa, open('b.txt') as fb: ...
lub zastosować contextlib.ExitStack dla bardziej skomplikowanych przypadków.
Czym różni się pisanie menedżera kontekstu jako klasy z enter/exit i jako generator z dekoratorem @contextmanager?
Dekorator @contextmanager z modułu contextlib pozwala na prostsze zrealizowanie menedżera, przez yield:
from contextlib import contextmanager @contextmanager def open_file(name): f = open(name) try: yield f finally: f.close() with open_file('file.txt') as f: ...
Programista otwiera plik za pomocą open(), zapisuje do niego, i zapomina wywołać close() — plik może pozostać otwarty (szczególnie w przypadku błędów), dane nie są zapisywane.
Plusy:
Minusy:
Wszędzie używany with open() jako standard (lub custom managed resource), menedżer kontekstu poprawnie realizuje zwalnianie zasobu.
Plusy:
Minusy: