Шаг 178.
Python: сборник рецептов.
Метапрограммирование. Легкий способ определения менеджеров контекста

    На этом шаге мы рассмотрим способ решения этой задачи.

Задача

    Вы хотите реализовать новые менеджеры контекста для использования с инструкцией with.

Решение

    Один из самых простых способов написания нового менеджера контекста - использовать декоратор @contextmanager из модуля contextlib. Вот пример менеджера контекста, подсчитывающего время выполнения блока кода:

>>> import time
>>> from contextlib import contextmanager
>>> @contextmanager
def timethis(label):
    start = time.time()
    try:
        yield
    finally:
        end = time.time()
        print('{}: {}'.format(label, end - start))

>>> # Пример
>>> with timethis('counting'):
    n = 10000000
    while n > 0:
        n -= 1

counting: 1.9680047035217285
>>> 

    В функции timethis() весь код перед yield выполняется как метод __enter__() менеджера контекста. Весь код после yield выполняется как метод __exit__(). Если имеет место исключение, оно возбуждается в инструкции yield.

    Вот немного более продвинутый пример менеджера контекста, который реализует некую транзакцию на объекте списка:

>>> from contextlib import contextmanager
>>> @contextmanager
def list_transaction(orig_list):
    working = list(orig_list)
    yield working
    orig_list[:] = working

    Идея в том, что изменения вносятся в список только в том случае, если при выполнении всего блока кода не возбуждаются исключения. Вот пример:

>>> items = [1, 2, 3]
>>> with list_transaction(items) as working:
        working.append(4)
        working.append(5)

        
>>> items
[1, 2, 3, 4, 5]
>>> with list_transaction(items) as working:
	working.append(6)
	working.append(7)
	raise RuntimeError('oops')


Traceback (most recent call last):
  File "<pyshell#16>", line 4, in <module>
    raise RuntimeError('oops')
RuntimeError: oops
>>> items
[1, 2, 3, 4, 5]
>>> 


Обсуждение

    Чтобы написать менеджер контекста, обычно вы определяете класс с методами __enter__() и __exit__():

import time

class timethis:
    def __init__(self, label):
        self.label = label

    def __enter__(self):
        self.start = time.time()

    def __exit__(self, exc_ty, exc_val, exc_tb):
        end = time.time()
        print('{}: {}'.format(self.label, end - self.start))

    Хотя это несложно, но все же более утомительно, чем писать простые функции с декоратором @contextmanager.

    @contextmanager на самом деле используется для написания замкнутых на себя функций с управлением контекстом. Если вам нужно реализовать поддержку инструкции with каким-либо объектом (например, файлом, сетевым соединением или блокировкой), вам все равно придется отдельно реализовать методы __enter__() и __exit__().

    На следующем шаге мы рассмотрим выполнение кода с локальными побочными эффектами.




Предыдущий шаг Содержание Следующий шаг