Как стать автором
Обновить

Context manager в рамках языка Python

Уровень сложностиПростой
Время на прочтение5 мин
Количество просмотров20K

Beautiful is better than ugly

“Always code as if the guy who ends up maintaining your code will be a violent psychopath who knows where you live.” - John Wood (possibly)

“Всегда пишите код так, будто в дальнейшем поддерживать его будет злобный психопат, который знает где ты живешь.” - (Мой нехудожественный перевод).

Python - это язык программирования, уделяющий много внимания тому, как мы пишем код. Самый первый пункт Zen of Python, принципов разработки на Python от его BDFL: “Beautiful is better than ugly”. Красивое лучше уродливого. Это само по себе простое и понятное утверждение, вынесенное на первое место в дзэне, напоминает нам простую истину - мы пишем код для людей, а не для машин. Машине для исполнения программы хватит нулей и единиц в бинарном файле, человек же куда более требователен.

Именно поэтому в разработке кода мы стараемся использовать все доступные средства, для того чтобы сделать его удобным для чтения и понятным человеку. В Python множество инструментов, которые могут помочь улучшить читаемость кода, и Context manager, о котором дальше пойдет речь, один из них.

Контекст

Прежде чем перейти к менеджеру контекста, вспомним что такое сам контекст. 

# Запись в файл без использования менеджера контекста

file = open("demo_1.txt", "w")

try:
    file.write("1!")

finally:
    # Убеждаемся что первый Файл был закрыт.
    file.close()

file = open("demo_2.txt", "w")

try:
    file.write("2!")

finally:
    # Убеждаемся что второй Файл был закрыт.
    file.close()

Блоки повторяющегося кода для работы с файлами demo_1.txt и demo_2.txt могут быть разбиты на отдельные шаги:

# Вход
file = open("demo_1.txt", "w")
# Собственно работа с файлом
file.write("1!")
# Выход
file.close()

В данном примере контекстом будет вызов метода write. 

13 мая 2005 года вышел PEP-343, который определил новое ключевое слово with, а также методы __enter__() и __exit__(), которые выполняются соответственно при входе и выходе из блока with.

# Запись в файл c использованием менеджера контекста

with open("demo_1.txt", "w") as file:
    file.write("1!")

with open("demo_2.txt", "w") as file:
    file.write("2!")

Количество дуплицированного кода уменьшилось, читабельность улучшилась, функционал остался прежним.

Менеджер контекста

Менеджером контекста называется объект, реализующий вышеобозначенные методы __enter__() и __exit__().

В примере выше функция open может выглядеть как менеджер контекста, однако это не совсем так — им является результат ее выполнения, а именно файловый дескриптор.

Реализация

Простейшая реализация

Рассмотрим пример использования менеджера контекста для отсчета времени выполнения некоторого блока кода. Пример взят из реального проекта, хоть и несколько упрощен для наглядности.

# Без использования менеджера контекста

start = time.clock()
…
foo(...)
…
executionl_time = time.clock() - start
print(float(executionl_time))

start = time.clock()
…
bar(...)
…
executionl_time = time.clock() - start
print(float(executionl_time))

Создадим менеджер контекста timer

# Менеджер контекста часто определяется в отдельном файле.
# В этом конкретном случае это был файл Utils.py

class timer(object):
    def __enter__(self):
        self.t = time.clock()
        return self

    def __exit__(self, type, value, traceback):
        self.e = time.clock()

    def __float__(self):
        return float(self.e - self.t)

# Код в другом файле
with timer() as t1:
    …
    foo(...)
    …
print(t1)

with timer() as t2:
    …
    bar(...)
    …
print(t2)

Стоит отметить, что метод __exit__() всегда принимает 4 аргумента в следующем порядке: 

  • self - ссылка на объект, служит для обращения к собственным переменным и методам. 

  • exception_type - тип исключения.

  • exception_value - объект исключения.

  • traceback - объект, содержащий информацию о последовательности вызовов, которые предшествовали исключению.

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

Реализация с использованием contextmanager

В PEP 343 также был представлен и более элегантный и эффективный способ создания менеджера контекста, а именно использование декортора contextmanager.

Генератор созданный с помощью декоратора contextmanager возвращает объект с автоматически созданными необходимыми методами __enter__() и __exit__(). В таком случае метод __enter__() возвращает next(), выполняя код до ключевого слова yield. __exit__() же, пытаясь получить второй next(), выполняет код после.

from contextlib import contextmanager

@contextmanager
def context_manager():
    # Внутри вызова __enter__
    print("Enter")
    try:
        yield
    finally:
        # Внутри вызова __exit__
        print("Exit")

with context_manager():
    print("Hello")
    
# >> Enter
# >> Hello
# >> Exit

Именно последний вариант стал общепринятым стандартом разработки.

Использование

Возможности использования менеджера контекста в разработке куда шире, чем многие привыкли думать. Наиболее часто упоминается пример с открытием файла для чтения или записи, а также создание соединения при работе с базами данных. Однако, я бы хотел перечислить несколько других мест, где использование менеджера контекста не только оправдано, но и приносит значительный вклад в упрощение как разработки кода, так и его поддержки в дальнейшем. 

Следующие примеры взяты из нескольких наиболее популярных open-source проектов на Python. Каждый пример содержит определение менеджера контекста и его использование.

Scrapy 

https://github.com/scrapy/scrapy

В репозитории scrapy, созданном для сканирования и парсинга веб страниц, можно найти следующий пример использования менеджера контекста. Здесь он использован с целью выставления временных переменных среды, возвратом к предыдущему состоянию после выхода из контеста, выполнения блока кода под оператором with.

# scrapy\scrapy\utils\misc.py
@contextmanager
def set_environ(**kwargs):
    """Temporarily set environment 
       variables inside the context … """

    original_env = {k: os.environ.get(k) for k in kwargs}
    os.environ.update(kwargs)
    try:
        yield
    finally:
        for k, v in original_env.items():
            if v is None:
                del os.environ[k]
            else:
                os.environ[k] = v

# Где-то в другом файле
…
with set_environ(SCRAPY_CHECK='true'):
    for spidername in args or spider_loader.list():
        spidercls = spider_loader.load(spidername)
…

Rich

https://github.com/Textualize/rich

Менеджер контекста может быть использован и просто для улучшения читаемости кода. Предлагаю обратить внимание на имплементацию демо функционала в известном репозитории rich, созданном для того, чтобы предоставить функционал для работы с текстом. Каждое действие здесь, в коде демо презентации, сопровождается небольшой паузой по окончанию выполнения для улучшения восприятия зрителем. Вместо того, чтобы вызывать time.sleep каждый раз, разработчик нашел элегантное решение с менеджером контекста beat.

# rich\examples\table_movie.py
BEAT_TIME = 0.04

@contextmanager
def beat(length: int = 1) -> None:
    yield
    time.sleep(length * BEAT_TIME)

# Где-то в другом файле
…
with beat(10):
    table.add_column("Release Date", no_wrap=True)

with beat(10):
    table.add_column("Title", Text.from_markup("[b]Total", justify="right"))
…

Cert

https://github.com/certbot/certbot

Данный пример взят из репозитория cert, созданным облегчить работу с сертификатами для https соединения. В нем разработчик использует менеджер контекста для того, чтобы гарантировать возврат к старой маске разрешений пользователя после того, как выполнится блок кода.

# certbot\certbot\certbot\compat\filesystem.py
@contextmanager
def temp_umask(mask: int) -> Generator[None, None, None]:
    """
    Apply a umask temporarily, meant to be used in a `with` block. Uses the Certbot
    implementation of umask.

    :param int mask: The user file-creation mode mask to apply temporarily
    """

    old_umask: Optional[int] = None
    try:
        old_umask = umask(mask)
        yield None
    finally:
        if old_umask is not None:
            umask(old_umask)

# Где-то в другом файле
…
with filesystem.temp_umask(0o022):
    util.set_up_core_dir(...)
…

Заключение

Менеджер контекста — инструмент, который может и должен быть использован для организации кода в случае, когда это работает на улучшение читаемости или уменьшение дупликации кода. Хороший признак того, что стоит задуматься над созданием менеджера контекста, это обнаружение повторяющегося блока кода с явно выраженными подготовкой перед и завершением после выполнения основной функциональности.

Теги:
Хабы:
Всего голосов 9: ↑9 и ↓0+9
Комментарии10

Публикации

Информация

Сайт
hr.auriga.ru
Дата регистрации
Дата основания
Численность
501–1 000 человек
Местоположение
Россия