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('SuppressOutput.__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('SuppressOutput.__exit__()')
with SuppressOutput() as stdout_write:
    print('That won\'t be printed')
    stdout_write('But this one will be printed\n')
SuppressOutput.__enter__()
But this one will be printed
SuppressOutput.__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 0x7f8a880f18c0>

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!