Menedżery kontekstu

PEP 343 - The “with” statement

https://docs.python.org/3/library/stdtypes.html#context-manager-types

https://docs.python.org/3/reference/datamodel.html#with-statement-context-managers

WPROWADZENIE

W Pythonie 2.6 i Pythonie 3 pojawiła się nowa instrukcja związana z wyjątkami - with z opcjonalnym as [PEP 343]. Instrukcja została zaprojektowana do pracy z obiektami menedżerów kontekstu obsługującymi protokół oparty na metodach. Instrukcja with/as ma być alternatywą dla zwykłego zastosowania try/finally.


# Składnia podstawowa.
# Wyrażenie ma zwracać obiekt obsługujący protokół zarządzania kontekstem.
with wyrażenie [as zmienna]:
    instrukcje

# Czytanie danych z pliku.
# Plik zostanie zamknięty nawet gdy wystąpi wyjątek.
with open(infile_name, 'r') as infile:
    read_data = infile.read()

# How to open a file using the open with statement.
# http://stackoverflow.com/questions/9282967/how-to-open-a-file-using-the-open-with-statement

def filter(text, infile_name, outfile_name):
    '''Read a list of names from a file line by line into an output file.
    If a line begins with a particular name, insert a string of text
    after the name before appending the line to the output file.
    '''
    with open(outfile_name, 'w') as outfile:
        with open(infile_name, 'r', encoding='utf-8') as infile:
            for line in infile:
                if line.startswith(text):
                    line = line[0:len(text)] + ' - Truly a great person!\n'
                outfile.write(line)

# W Py2.7 i Py3.1+ można użyć wielokrotnie open(),
# co jest równoważne zagnieżdżaniu with.
# http://docs.python.org/reference/compound_stmts.html#the-with-statement

with open(outfile_name, 'w') as outfile, \
    open(infile_name, 'r', encoding='utf-8') as infile:
        instrukcje

PROTOKÓŁ ZARZĄDZANIA KONTEKSTEM

Opis działania instrukcji with.


# Przykład działania protokołu zarządzania kontekstem.

class ContextManager:

    def __init__(self):   # metoda opcjonalna
        pass

    def message(self, argument):
        print("wykonywanie {}".format(argument))

    def __enter__(self):
        print("rozpoczęcie bloku with")
        return self   # czasem może być inny obiekt

    def __exit__(self, exception_type, exception_value, exception_traceback):
        # typ, wartość, ślad wyjątku
        if exception_type is None:   # brak wyjątku
            print("normalne wyjście")
            return True
        else:
            print("zgłoszenie wyjątku {}".format(exception_type))
            return False    # przekazanie wyjątku
            # return True   # porzucenie wyjątku

# Zastosowanie.

with ContextManager() as context:
    context.message("test 1")
    print("wiersz osiągnięty")

with ContextManager() as context:
    context.message("test 2")
    raise TypeError
    print("wiersz nie zostanie osiągnięty")

print("po with ...")

Menedżery kontekstu są zaawansowanymi narzędziami, przeznaczonymi dla osób tworzących narzędzia. Dodatkowe narzędzia służące do tworzenia kodu menedżerów kontekstu udostępnia moduł contextlib [PEP 343].

UNITTEST

https://docs.python.org/3/library/unittest.html#unittest.TestCase.assertRaises

W Pythonie 2.7 i 3.1 pojawiła się możliwość użycia assertRaises() jako menedżera kontekstu. Można badać szczegóły wyzwolonego w teście wyjątku. Warto prześledzić przykłady z dokumentacji [25.3 unittest].


with self.assertRaises(SomeException) as context:
    instrukcja   # pojedyńcza instrukcja wyzwalająca wyjątek

# Sprawdzenie pewnego kodu błędu (na zewnątrz instrukcji with).
self.assertEqual(context.exception.error_code, 3)

# Można sprawdzić, czy dostaliśmy daną klasę wyjątku,
# czy może jego rodzica.
self.assertEqual(context.exception.__class__, SomeException)
self.assertTrue(isinstance(context.exception, SomeException))

# Sprawdzenie występowania pewnego tekstu w komunikacie.
self.assertTrue("NoneType" in str(context.exception))

W Pythonie 3.4 pojawił się menedżer kontekstu subTest(), który pomaga tworzyć testy o bardziej czytelnych komunikatach. Zwykle używa się subTest(), gdy mamy wiele mało różniących się testów, np. zmieniają się tylko parametry wywołania danej funkcji.


# https://docs.python.org/3/library/unittest.html
# https://www.caktusgroup.com/blog/2017/05/29/subtests-are-best/

def is_even(n):
    return n % 2 == 0

class NumbersTest(unittest.TestCase):

    def test_even(self):
        """
        Test that numbers between 0 and 5 are all even.
        """
        for i in range(0, 6):
            with self.subTest(i=i):   # Py3
                self.assertEqual(i % 2, 0)
                # self.assertTrue(is_even(i))   # wersja z funkcją

# Dzięki subTest() otrzymamy informację dla jakiego 'i' były błędy.

FUNKCJE CZYSZCZĄCE

W pewnych sytuacjach potrzebne są działania czyszczące na końcu przetwarzania, które zostaną wykonane niezależnie od wystąpienia wyjątków. W ramach jednego modułu można wykorzystać try/except/finally lub menedżera kontekstu with.

Jeżeli pewne działania mają być wykonane przy normalnym zakończeniu działania interpretera, dawny sposób polegał na rejestrowaniu jednej funkcji bezparametrowej func() w funkcji systemowej sys.exitfunc(func). Obecnie zalecanym sposobem działania jest korzystanie z modułu 'atexit', dzięki któremu można rejestrować wiele funkcji wykonujących prace czyszczące. Moduł 'atexit' jest obecny w Pythonie 2 i 3.


# https://docs.python.org/3/library/atexit.html
# https://pymotw.com/2/atexit/

import atexit

def my_cleanup(name):
    print('my_cleanup({})'.format(name))

atexit.register(my_cleanup, 'first')
atexit.register(my_cleanup, 'second')
atexit.register(my_cleanup, 'third')
# Komunikaty po normalnym zakończeniu pracy interpretera Pythona:
# my_cleanup(third)   # działają od końca, jak stos
# my_cleanup(second)
# my_cleanup(first)