Menadżery kontekstu

Wprowadzenie

Często spotykany w zarządzaniu zasobami jest następujący idiom:

do_setup()
try:
    do_task()
except SomeError:
    handle_the_error()
finally:
    do_cleanup()

Wyrażenie with

Aby uprościć i uodpornić się na błędy programisty, od Pythona 2.5 wzwyż dostępne jest wyrażenie with.

Menedżer kontekstu (context manager) jest odpowiedzialny za zarządzanie zasobami wewnątrz bloku kodu.

Najczęściej tworzy te zasoby na początku bloku, a zwalnia na końcu.

Na przykład, menadżer kontekstu dla plików upewnia się, że pliki zostały prawidłowo zamknięte po zakończeniu bloku, nawet jeśli zostanie zgłoszony wyjątek.

with open('myfile.txt', 'wt') as f:
    f.write('foo bar')

Odpowiednikiem bloku:

with VAR = EXPR:
    BLOCK

jest zapis:

VAR = EXPR
VAR.__enter__()
try:
    BLOCK
finally:
    VAR.__exit__()

Protokół menadżera kontekstu

Menedżer kontekstu jest klasą posiadającą dwie metody specjalne:

  • __enter__ - metoda wywoływana na samym początku bloku wewnątrz with.

  • __exit__ - metoda jest odpowiednikiem finally:, wywoływana po zakończeniu bloku with.

Poniżej przedstawiono przykładowy, prosty menadżer kontekstu:

class Context:
    def __init__(self):
        print('__init__()')
    
    def __enter__(self):
        print('__enter__()')
        return self
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        print('__exit__()')
with Context():
    print("Doing work inside context")
__init__()
__enter__()
Doing work inside context
__exit__()

Metoda __enter__

Wartością zwracaną przez menadżera kontekstu w funkcji __enter__ może być obiekt, który zostanie przypisany do zmiennej występującej po as:

import sys

def blackhole(*args, **kwargs):
    pass

class SuppressOutput:
    def __enter__(self):
        print('Context.__enter__()')
        self.write, sys.stdout.write = sys.stdout.write, blackhole
        return self.write

    def __exit__(self, exc_type, exc_val, exc_tb):
        sys.stdout.write = self.write
        print('__exit__()')
with SuppressOutput() as stdout_write:
    print('That won\'t be printed')
    stdout_write('But this one will be printed\n')
Context.__enter__()
But this one will be printed
__exit__()

Metoda __exit__

Do metody __exit__ trafia informacja o wyjątkach, jakie pojawiły się bloku with.

  • Jeśli metoda __exit__ zwraca true, to wyjątek został obsłużony przez menadżera kontekstu.

  • Jeśli zwrócona zostanie wartość false, to wyjątek będzie propagowany dalej.

class Context:
    def __enter__(self):
        pass
    
    def __exit__(self, excpt_type, excpt_val, excpt_tb):
        print("Exception type:", excpt_type)
        print("Exception value:", excpt_val)
        print("Traceback object:", excpt_tb)
        return True  # or False
with Context():
    x = 2
Exception type: None
Exception value: None
Traceback object: None
with Context():
    x = 2 / 0
Exception type: <class 'ZeroDivisionError'>
Exception value: division by zero
Traceback object: <traceback object at 0x7f2aa54664c0>

contextlib.contextmanager

W prostych przypadkach zamiast tworzyć klasę, możemy skorzystać z gotowego dekoratora zawartego w module contextlib, który konwertuje składnię funkcji do postaci menadżera kontekstu:

from contextlib import contextmanager

@contextmanager
def make_context():
    try:
        prepare_resource()
        yield context_object
    except RuntimeError as err:
        handle_exception_here()
    finally:
        do_clean_up()

Przykładowy prosty menadżer kontekstu napisany z użyciem contextmanager:

from contextlib import contextmanager

@contextmanager
def Shouter():
    print('going in')
    yield
    print('coming out')

with Shouter():
    print('inside')
going in
inside
coming out

Jeżeli chcemy obsłużyć rzucone przez funkcję wyjątki, możemy to zrobić w następujący sposób:

@contextmanager
def Shouter():
    print('going in')
    try:
        yield
    except Exception:
        print('Error!')
    else:
        print('no error')
with Shouter():
    pass
going in
no error
with Shouter():
    print(1/0)
going in
Error!