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(...)
…
Заключение
Менеджер контекста — инструмент, который может и должен быть использован для организации кода в случае, когда это работает на улучшение читаемости или уменьшение дупликации кода. Хороший признак того, что стоит задуматься над созданием менеджера контекста, это обнаружение повторяющегося блока кода с явно выраженными подготовкой перед и завершением после выполнения основной функциональности.
Комментарии (10)
valentinmk
00.00.0000 00:00-1Я чуть мозг не сломал с последним примером.
old_mask = None
, потомold_mask = umask(mask)
, а финалкаumask(old_mask)
. Наивно получается, что устанавливается новая маска, хотя вокруг написано про "временно".
Я знаю umask, но я не знаю как именно работает umask, ну не было у меня опыта поработать плотно.
Полез в код:
https://github.com/certbot/certbot/blob/master/certbot/certbot/compat/filesystem.py#L64Добавь плиз для контекста, что ОНО возвращает предыдущую маску
def umask(mask: int) -> int: """ Set the current numeric umask and return the previous umask. On Linux, the built-in umask method is used. On Windows, our Certbot-side implementation is used. :param int mask: The user file-creation mode mask to apply. :rtype: int :return: The previous umask value. """ if POSIX_MODE: return os.umask(mask) previous_umask = _WINDOWS_UMASK.mask _WINDOWS_UMASK.mask = mask return previous_umask
funca
00.00.0000 00:00+1Контекст менеджеры в питоне это компромисс между читабельностью и явностью (в смысле explicit is better than implicit). Используя контекст менеджер нужно постоянно держать в голове его особенности: Single Use, Reusable or Reentrant, помноженные на потокобезопасность и возможность использовать с async - пользовательский код будет выглядеть абсолютно одинаково, а поведение существенно разным.
Например:
# Где-то в другом файле … with filesystem.temp_umask(mask): util.set_up_core_dir(...)
Как поведет себя этот код будучи вызванным одновременно в разных потоках с разными значениями mask?
Buchachalo
00.00.0000 00:00+1Ох мне недавно понадобилось сделать что то подобное, голову сломал.
Если на примерах покажете то же создание и завершение сессии в БД, либо к стороннему API, в рамках синхронного/асинхронного подхода. Я думаю сообществу это бы понадобилось.
Andrey_Solomatin
00.00.0000 00:00+1Обычно в документации к коннекторам это есть.
Если какое-то API требует действия после работы, но не имеет контекстного менеджера, я взял себе за правило делать его самостоятельно, благо это очень легко.
Асинхронные контекстные менеджеры тоже есть, но я с ними не работал, но в общем идея та же, только внутри можно await использовать.Buchachalo
00.00.0000 00:00__aenter__ и __aexit__, хоть то и не большое отличие, но реализация другая.
Andrey_Solomatin
00.00.0000 00:00В Python множество инструментов, которые могут помочь улучшить читаемость кода, и Context manager, о котором дальше пойдет речь, один из них.
Не уверен, что контекстный менеджеры для читаемости, это больше инструмент позволяющий два завязанных по времени выполнения действий, выполнить в заданном порядке и с гарантией вызова обоих действий. Такой подход избавляет от возможных человеческих ошибок. По моему опыту почти все, кто открывают файл руками, не закрывают его правильно.
NeoCode
Это что-то типа RAII в С++.
piroxd Автор
Что-то вроде того. Только куда больше простор для использования. Едва ли не для всего что может быть представлено в виде поготовка-действие-завершение
iuabtw
Жаль, что в статье не рассмотрено применение в юнит тестах - удобно всякую фигню мокать на ходу.
Ещё в питоне можно вызвать несколько контекстов одновременно - через вложенные with или вот так
Вопрос со звёзочкой: в сколько контекстов можно зайти одновременно? :D