Mateusz Mazurek – programista z pasją

Blog o Pythonie i kilku innych technologiach. Od developera dla wszystkich.

Programowanie Programowanie webowe

Menadżer kontekstu w Pythonie

Cześć.

Menadżery kontekstu w Pythonie to dość użyteczne narzędzie, którego głównym zadaniem jest…. Chronić programistów przed ich zapominalstwem. Już na samym początku nauki programowania spotykamy się enigmatyczną instrukcją with, która w połączeniu z funkcją open pozwala na pracę z plikami. Dziś przyjrzymy się owej instrukcji trochę głębiej. Zapraszam.

Po co nam menadżery kontekstu?

Odpowiedź jest prosta – żeby nie musieć pamiętać o tym, że zasoby których używamy, należy zwalniać. Takim najbardziej powszechnie używanym zasobem jest pamięć. I jak pewnie wiesz, alokujemy ją tworząc zmienne, ale… Nigdy nie zwalniamy! Pamięć jest tak powszechnie używanym zasobem, że Python, podobnie jak inne języki, sam zarządza zwalnianiem pamięci dzięki algorytmowi zliczania referencji i aplikacji zwanej garbage collector.

Pamięć nie jest jedynym zasobem którego możemy używać. Pisząc o menadżerach kontekstu nie da się nie wspomnieć o powszechnie używanej funkcji open – czyli bezsprzecznie najpopularniejszym wbudowanym obiektem tego typu.

Funkcję open możemy używać na dwa podstawowe sposoby. Pierwszy, tradycyjny sposób to:

1
2
3
4
file = open("text.txt")
lines = file.readlines()
print(lines)
file.close()

a drugi, używający menadżera to:

1
2
3
with open("text.txt") as file:
    lines = file.readlines()
    print(lines)

To co warto tu zauważyć, to fakt, że w przypadku tradycyjnego użycia, musimy pamiętać o wywołaniu metody close(). Natomiast w przypadku menadżera jesteśmy z tego zwolnieni.

Wygoda rośnie wraz z ilością plików na których pracujemy, ponieważ nic nie stoi na przeszkodzie, żeby otworzyć dwa pliki i np. przepisać zawartość jednego do drugiego. Wciąż nie musząc pamiętać o zamykaniu otwartych plików:

1
2
with open("text.txt") as file, open("text2.txt", "w") as file2:
    file2.write(file.read())

Jak to działa pod spodem?

Żeby dobrze zrozumieć jak działa menadżer kontekstu, napiszmy własny obiekt OwnOpen który będzie dostarczał podobna funkcjonalność co oryginalna funkcja open:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class OwnOpen:
    def __init__(self, filename, mode="r"):
        self._filename = filename
        self._mode = mode
        self._file_handler = None

    def __enter__(self):
        self._file_handler = open(self._filename, self._mode)
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self._file_handler.close()

    def own_read(self):
        return self._file_handler.read()

Najważniejsze w tym snippecie są definicje metod __enter__ i __exit__.

Pierwsza ze wspomnianych metod, czyli __enter__ wykonuje się przed kodem znajdującym się pod zakresem słówka with a druga – po wykonaniu się tego kodu. Pierwsza powinna zwracać obiekt na którym będziemy pracować, który potem będzie dostępny w zmiennej wskazanej po słówku as, druga zaś nie musi nic zwracać.

Jeśli w obrębie kodu znajdującego się pod słówkiem with zostanie rzucony wyjątek, dowiemy się o tym analizując argumenty funkcji __exit__ – pozwala to na jego ewentualne obsłużenie wyjątku. Jeśli wyjątek ma być ukryty, funkcja __exit__ powinna zwracać True.

Popatrz na poniższy kod:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class OwnOpen:
    def __init__(self, filename, mode="r"):
        self._filename = filename
        self._mode = mode
        self._file_handler = None

    def __enter__(self):
        self._file_handler = open(self._filename, self._mode)
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self._file_handler.close()
        if isinstance(exc_val, TypeError):
            return
        return True

    def own_read(self):
        return self._file_handler.read()


with OwnOpen("text.txt") as file, open("text2.txt", "w") as file2:
    # raise Exception() or TypeError()
    file2.write(file.own_read())

Jeśli przy tak zdefiniowanej funkcji __exit_, pod słówkiem with pojawi się wyjątek TypeError to zostanie on rzucony (ponieważ __exit__ zwróci None), natomiast każdy inny wyjątek zostanie ukryty.


Czekaj, stop!

Podoba Ci się to co tworzę? Jeśli tak to zapraszam Cię do zapisania się na newsletter:
a w ramach prezentu otrzymasz całkowicie za darmo, dwa dokumenty PDF „6 (nie zawsze oczywistych) błędów popełnianych podczas nauki programowania” który jest jednym z efektów ponad siedmioletniej pracy i obserwacji rozwoju niejednego programisty oraz „Wstęp do testowania w Pythonie”, będący wprowadzeniem do biblioteki PyTest.
Jeśli to Cię interesuje to zapraszam również na swoje social media.

Alternatywny sposób implementacji menadżera

Zabawa z __enter__ i __exit__ jest fajna, ale generuje trochę za dużo kodu. Python pozwala zrobić to krócej, korzystając ze specjalnego dekoratora:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from contextlib import contextmanager


@contextmanager
def own_open(filename, mode="r"):
    file_handler = open(filename, mode)
    try:
        yield file_handler
    finally:
        file_handler.close()


with own_open("text.txt") as file, open("text2.txt", "w") as file2:
    file2.write(file.read())

Ten sprytny skrót wymaga od nas stworzenia funkcji generującej, która będzie „podzielona” słówkiem yield na kawałki kodu odpowiadającej odpowiednio:

  • to co przed yield to metoda __enter__
  • yield to kod pod słówkiem with
  • to co po yield to metoda __exit__

Fajne, prawda?

Co poza otwieraniem plików?

Fajnie, że sobie powiedzieliśmy czym są menadżery kontekstu ale nadal mamy tylko ten jeden przykład. Trochę mało. Ale spokojnie, menadżery kontekstu są dość często używane, spotkamy je na przykład w takich zastosowaniach:

  • podczas pracy z bazą danych i transakcyjnością, po prostu zabierają nam z barków konieczność pamiętania o begin oraz commit (lub rollback)
  • podczas pracy z mutexami/semaforami, dbają za nas o to, by zwalniać dostęp do sekcji krytycznej
  • różnego rodzaju połączenia sieciowe, gdzie pracujemy w trybie „nawiąż połączenie, wyślij dane, zakończ połączenie”
  • w testach jednostkowych np. do określenia zasięgu patchowania
  • itp..

Czasem menadżer kontekstu przydaje się w sytuacjach gdzie chcemy w jakiś sposób zbadać kawałek kodu. Np. zmierzyć czas wykonywania:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from contextlib import contextmanager
from time import time, sleep


@contextmanager
def time_it():
    start = time()
    try:
        yield
    finally:
        stop = time()
        print(f"Duration: {stop-start}")


with time_it():
    sleep(1)

Podsumowanie

I to by było na tyle. Menadżery kontekstu to potężne narzędzie, którego głównym zadaniem jest odciążenie programistów z konieczności pamiętania o zwalnianiu zasobów. Mam nadzieję, że artykuł okazał się ciekawy i że dzięki niemu zaczniesz świadomej używać składni with .. as a może i gdzieniegdzie tworzyć własne menadżery.

Dzięki za wizytę,
Mateusz Mazurek

A może wolisz nowości na mail?

Subskrybuj
Powiadom o
guest

Witryna wykorzystuje Akismet, aby ograniczyć spam. Dowiedz się więcej jak przetwarzane są dane komentarzy.

5 komentarzy
Inline Feedbacks
View all comments
zordonmax

Świetne treści, ale layout bloga jest okropny.

zordonmax

Zbyt duża ilość sekcji trochę przytłacza. Brak możliwości przełączenia na dark mode.

Mateusz H.

Fajny artykuł. Trochę mi przybliżył po co w ogóle jest Menadżer Kontekstu.

Mi się layout strony podoba jednak i tak głównie patrzę na treść która jest super.