Context Manager ochroni Twoje plecy!
- Jarosław Piszczała
- 11 Jun, 2020
- 11 Jun, 2020
Menadżer kontekstu (ang. context manager) to obiekt, który odpowiednio zarządza danym zasobem, zapewniając, że zostanie on odpowiednio zamknięty. Przez zamknięty mam tu na myśli czyszczenie, zwalnianie zasobów oraz sprzątanie po wykonaniu bloku kodu. Dobrym przykładem takiego obiektu, jest użycie open
w Pythonie. Korzystając z niego w domyślny sposób, otwieramy plik, odczytujemy pewien jego element, a następnie go zamykamy.
Jednak open
to też menadżer kontekstu, aby z niego skorzystać, należy skorzystać z konstrukcji with ... as ... :
, gdzie samo as
nie jest wymagane gdy nie potrzebujemy w danym kontekście korzystać z jego obiektu. Więc po with
podajemy nasz menadżer, czyli open()
. Następnie, aby mieć dostęp do otwieranego pliku, dodajemy as
i zmienną, ja użyję zmiennej f
.
Dzięki ten konstrukcji nie musimy korzystać z f.close()
, plik zostanie zamknięty samoczynnie po wyjściu z kontekstu, czyli po zakończeniu działania na kodzie, który znalazł się w jej ciele poprzez wcięcie.
Baza
Aby pokazać więcej na ten temat, utworzyłem klasę Storage
, która będzie symulować magazyn danych. Dla zabawy, przy próbie pobrania czegokolwiek z zasobu możemy dostać wyjątek StorageException
. Aby z niego korzystać, musimy uprzednio zablokować dostęp do zasobu, a na koniec zasób zwolnić.
Domyślne użycie tej klasy wyglądałoby następująco.
O ile nie wystąpiłby błąd, wszystko byłoby ok. Jednak czasem przy wywołaniu możemy dostać błąd. Spowoduje to nie wykonanie się ostatniej linii kodu, a to oznacza zablokowanie zasobu do momentu, gdy ktoś manualnie go odblokuje lub proces utrzymujący dostęp do zasobu zostanie zabity i system przywróci zasób do swojej puli.
Dispose Pattern
Taki problem rozwiązuje wzorzec projektowy Dispose
, który służy właśnie do zamykania zasobów w przypadku gdy pojawi się błąd. Jego najprostsza implementacja w naszym przypadku korzysta z try finally, więc jeżeli cokolwiek wydarzy się w naszym kodzie, to przed ostatecznym wywołaniem wyjątku i zakończeniem działania kodu wykona się kod sekcji finally
. Dzięki temu mamy pewność, że nasz zasób zawsze zostanie zamknięty, czy też w tym przypadku zwolniony.
Mała uwaga, nie próbuj w bloku finally
zwracać żadnej wartości. Spowoduje to nadpisanie tego, co powinno zostać zwrócone. Utracisz w ten sposób wartość zwracaną z kodu, lub zignorujesz wyjątek.
Elementy menadżera kontekstu
Przejdźmy do stworzenia własnego menadżera, bez korzystania z dokumentacji. Będziemy zatem obserwować błędy wyrzucane po uruchomieniu kodu, i dodawać do niego brakujące elementy. Więc w pierwszej kolejności tworzymy pustą klasę, i spróbujemy ją użyć z instrukcją with
.
Wyjątek, który się pojawił, mówi jasno, iż brakuje nam atrybutu enter
. Podejrzewam, że chodzi o metodę, dlatego dodaje do naszej klasy taką metodę. Następnie uruchamiam kod ponownie.
To ma sens. Mamy metodę enter
do otwierania pewnego kontekstu, ale potrzebujemy także exit
aby ten kontekst zakończyć. Dodajemy metodę i uruchamiamy kod ponownie.
Dostajemy informacje, że metoda exit()
przyjmuje aż 4 argumenty pozycyjne. Jeden to self
, resztę opakujemy w *args
, sprawdzimy potem, co się w nich kryje. Uruchamiamy ponownie.
Kod uruchomił się bez błędów. Udało się zdefiniować szkielet, jakiego wymaga Python. Popracujmy dalej nad tym, aby funkcja print
zwróciła None
. Podejrzewam, że metoda enter
powinna zwrócić jakiś obiekt, na którym będziemy pracować, tak więc dodaje return self
.
Tak jak podejrzewałem, aby móc pracować na obiekcie ograniczonym naszym kontekstem, w metodzie enter
należy ten obiekt zwrócić. Została ostatnia magiczna rzecz, a jest nią zbiór argumentów pozycyjnych w metodzie exit
. Dodajmy tam print
aby się przekonać.
Jest pusto, to dziwne. Jednak pamiętajmy, że wzorzec projektowy korzystał z obsługi błędów, więc może jego wywołanie coś zmieni.
Mamy wyjaśnienie. Metoda exit
dostaje informacje o wyjątkach, które się pojawiły. Dzięki temu możemy te wyjątki odpowiednio obsłużyć lub ich obsługę pominąć. Jest jeszcze jedna rzecz, którą ciężko by mi było wypracować. W momencie gdy exit
zwraca wartość domyślną None
, to wyjątek i tak zostanie wyrzucony. Aby do tego nie dopuścić, należy zwrócić z metody exit
wartość True
.
Implementacja za pomocą klasy
Ostateczna implementacja naszego kodu do obsługi zasobu za pomocą klasy wygląda zatem następująco:
Implementacja za pomocą funkcji
Ktoś mądry pomyślał, że dobrym pomysłem będzie tworzenie prostych menadżerów kontekstu opartych o generator. W taki sposób powstał dekorator contextmanager
z biblioteki wbudowanej contextlib, dzięki któremu możemy przekształcić generator w menadżera kontekstu. Sposób implementacji naszego problemu za pomocą tego narzędzia poniżej. Rozwiązanie to jest bardzo często używane, gdyż ma prostszą konstrukcję oraz dużo łatwiej go zaimplementować.
Przykłady
Skoro wiemy już, jak tworzyć context manager, to co można z nim zrobić? Jeden z najważniejszych przypadków pokazałem na powyższych przykładach, czyli dostęp do pewnych zasobów, które wymagają konkretnej operacji przed i po.
Code Timer
Inny ciekawy przykład, to stworzenie klasy do mierzenia czasu działania danego kawałka kodu. Z reguły jest to przykład na dekorator, ale wymaga on wtedy zamknięcia naszego kodu w funkcji. Licznik oparty o menadżera kontekstu nie ma tego ograniczenia.
Dodatkowo pojawia się tutaj prosta konfiguracja logera, dzięki której możemy logować informacje z naszego kodu. Korzystanie z logera jest dużo lepszym wzorcem niż korzystanie z print
w naszym kodzie, ponieważ loger możemy odpowiednio skonfigurować, aby dla przykładu wysyłał nasze logi do platformy , gdzie te logi możemy następnie przejrzeć.
Ignore Exceptions
Inną propozycją ode mnie, jest context manager, którego zadaniem będzie ignorowanie wszelkich błędów, które wystąpią. Nie chcemy jednak stracić informacji o tym, iż wystąpiły, dlatego odpowiednia informacja zostanie przekazana do logera.