loading...

12.05.2020

Уроки Python: Менеджер контекста (context manager)

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

Что такое менеджер контекста

Менеджер контекста — очень важная составляющая языка Python. Он позволяют как бы «обернуть» код и обычно используется для захвата и освобождения ресурсов, хотя бывают и другие варианты использования менеджера контекста.

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

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

Как я уже писал выше — менеджер контекста обычно используется в связке с конструкцией with ...:/with ... as ...: и классический пример использования менеджера контекста — это работа с файлом.

Пример работы с файлом:

with open('file.txt', mode='w') as f:
    f.write('Write some data')

В данном примере мы использовали конструкцию with ... as ...: для работы с файлом.

Что происходит в этом коде? В первой строке мы открываем файл (с помощью функции open), в переменной f теперь у нас file object. Во второй строке мы ‘входим’ в контекст и работаем уже в рамках этого контекста, где у нас доступна переменная f. С помощью строчки f.write() мы записываем строку 'Write some data' в файл . При дальнейшем выполнении кода будет произведён выход из менеджера контекста и файл будет автоматически закрыт (вызовется f.close() даже если произошла ошибка при записи). Данный код идентичен следующему коду:

f = open('file.txt', mode='w')
try:
    f.write('Write some data')
finally:
    f.close()

Другие полезные менеджеры контекста

В стандартной библиотеке Python есть и другие менеджеры контекста, например:

1. Из модуля zipfile (создаётся архив archive.zip и в него добавляется файл secret.txt):

with ZipFile('archive.zip', 'w') as zip_file:
    zip_file.write('secret.txt')

2. Из модуля subprocess:

with Popen(["ls"], stdout=PIPE) as proc:
    print(proc.stdout.read())

Пишем свой менеджер контекста

Написать свой менеджер контекста не сложнее, чем объявить функцию, хотя для полного понимания работы менеджера контекста надо понимать что такое генераторы (о них я расскажу в следующих статьях).

И так, есть два вариант создания менеджера контекста:

  1. Написать функцию (генератор) и обернуть её в декоратор contextlib.contextmanager (или contextlib.asynccontextmanager для асинхронной версии)
  2. Объявить класс с методами __enter__ и __exit__ (или __aenter__ и __aexit__ для асинхронной версии)

Используем декоратор @contextmanager

Как я уже написал выше — нам надо написать функцию (по правде это будет генератор) и обернуть её в декоратор. Официальная документация Python приводит такой пример:

from contextlib import contextmanager


@contextmanager
def managed_resource(*args, **kwds):
    # Code to acquire resource, e.g.:
    resource = acquire_resource(*args, **kwds)
    try:
        yield resource
    finally:
        # Code to release resource, e.g.:
        release_resource(resource)

Он полностью корректен, но слишком абстрактен и не особо подходит для обучения. Давайте напишем свой менеджер контекста для открытия файла:

from contextlib import contextmanager


@contextmanager
def open_file(path):
    try:
        print('Try to open file ...')
        file_obj = open(path, 'w')
        yield file_obj
    finally:
        print('Close file...')
        file_obj.close()

with open_file('some_file.txt') as file:
    file.write('Some data')

Результат выполнения этого кода:

Try to open file ...
9
Close file...

Видим что сначала вывелась первая строка с помощью print, потом количество записанных байт (это результат выполнения file.write("Some data")), а в конце — вторая строка с помощью print (см. блок finally в менеджере контекста).

Вот ещё простой пример менеджера контекста:

from contextlib import contextmanager

@contextmanager
def wrap_tags(tag):
    print('[{}]'.format(tag))
    yield
    print('[/{}]'.format(tag))

with wrap_tags('h1'):
    print('Hello')

Результат выполнения этого кода:

[h1]
Hello
[/h1]

Думаю в этом примере всё просто и понятно, если у вас есть вопросы — пишите в комментариях.

Класс менеджера контекста

Давайте рассмотрим вариант создания менеджера контекста с помощью класса. Для этого нам требуется написать класс с двумя методами: __enter__ и __exit__. Как понятно из названий этих методов — первый вызывается при «входе» в контекст, а второй при «выходе».

Давайте перепишем менеджер контекста для открытия файла на запись из прошлой части статьи:

class FileWrite:
    def __init__(self, path):
        self.path = path

    def __enter__(self):
        print('Try to open file ...')
        self.file = open(self.path, 'w')
        return self.file

    def __exit__(self, exc_type, exc_value, traceback):
        print('Close file...')
        self.file.close()
        return True

with FileWrite('some_file.txt') as f:
    f.write('Some data')

Код довольно простой и понятный. Единственный момент — метод __exit__ обязательно должен принимать 3 аргумента (можно конечно написать *exc, но не стоит):

  • exc_type — Тип исключения (если оно произошло в блоке менеджера контекста), либо None
  • exc_value — Объект исключения (опять же если оно произошло в блоке), либо None
  • traceback — Как понятно из названия — это объект трейсбэка, либо None

Результат выполнения кода:

Try to open file ...
Close file...

И у нас появился файл some_file.txt с текстом "Some data".

Асинхронный менеджер контекста

С версии Python 3.5 есть поддержка асинхронных менеджеров контекста. Для чего они нужны? Собственно точно для того же для чего и обычные менеджеры контекста, но используются если требуется выполнить некий асинхронный код (async/await). По правде отличий от обычных менеджеров контекста минимум так что давайте кратко рассмотрим примеры использования асинхронных вариантов.

Используем декоратор @asynccontextmanager

В том же пакете contextlib есть функция asynccontextmanager, она работает точно так же как и contextmanager и единственное отличие — функция, которую вы оборачиваете в этот декоратор должна быть async.
Давайте рассмотрим пример из документации (он очень простой и понятный):

from contextlib import asynccontextmanager

@asynccontextmanager
async def get_connection():
    conn = await acquire_db_connection()
    try:
        yield conn
    finally:
        await release_db_connection(conn)

async def get_all_users():
    async with get_connection() as conn:
        return conn.query('SELECT ...')

Здесь всё очень похоже на пример использования contextmanager с тем лишь отличием что весь этот код — асинхронный (ради чего, собственно, и применяется asynccontextmanager).

Класс асинхронного менеджера контекста

Здесь тоже ничего особо нового, просто вместо методов def __enter__ и def __exit__ используются методы async def __aenter__ и async def __aexit__.
Давайте рассмотрим полноценный простой пример (python 3.7!):

import asyncio


async def async_log(text: str):
    print('Log: {}'.format(text))


class AsyncContextManager:
    async def __aenter__(self):
        await async_log('Entering context')
        return self

    async def __aexit__(self, exc_type, exc, tb):
        await async_log('Exiting context')
        
    async def say(self, text: str):
        print(text)


async def main():
    async with AsyncContextManager() as async_manager:
        print('Hello!')
        await async_manager.say('Hello!!!')


asyncio.run(main())

Результат выполнения данного кода:

Log: Entering context
Hello!
Hello!!!
Log: Exiting context

Это полностью работоспособный пример, так что можете скопировать данный код в файл, сохранить и запустить.
Давайте разберём данный код.

Сначала мы объявили асинхронную функцию async_log, которая имитирует функцию логирования (например, сообщения могут отправляться на удалённый сервис с помощью HTTP запроса).
Далее объявлен класс AsyncContextManager в котором объявлены асинхронные методы __aenter__, __aexit__ необходимые для работы асинхронного менеджера контекста, и простой асинхронный метод say, который просто выводит на экран переданную ему в первом аргументе строку.
Ниже объявлена простая асинхронная функция main, которая является основной точкой входа программы и запускается с помощью конструкции asyncio.run(main()). Если вы используете Python версии, например, 3.6, то замените эту строку на код:

loop = asyncio.get_event_loop()
loop.run_until_complete(main())

Заключение

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

Posted in Python, БлогTaggs:
Write a comment