Несколько лет назад, в Python 2.5 добавили новое ключевое слово, под названием оператор with. Это новое ключевое слово позволяет разработчику создавать контекстные менеджеры. Но подождите, что же такое контекстный менеджер? Это удобные конструкции, которые позволяют разработчику настраивать что-нибудь и разрывать в автоматическом режиме. Например, вам может потребоваться открыть файл, вписать в него кучу всего и закрыть. Это классический пример работы контекстного менеджера. Фактически, Python создает один такой экземпляр автоматически каждый раз, когда вы открываете файл, используя оператор with:
1 2 |
with open(path, 'w') as f_obj: f_obj.write(some_data) |
В Python 2.4, вам нужно делать это старомодным способом:
1 2 3 |
f_obj = open(path, 'w') f_obj.write(some_data) f_obj.close() |
Это работает путем использования двух волшебных методов Python: __enter__ и __exit__. Давайте попробуем создать собственный контекстный менеджер, чтобы увидеть, как это работает на практике.
Создаем класс Context Manager
Вместо того, чтобы переписывать открытый метод Python, мы создадим контекстный менеджер, который создает связь с базой данных SQLite, и закрывает её по окончанию работы. Вот простой пример:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
import sqlite3 class DataConn: def __init__(self, db_name): """Конструктор""" self.db_name = db_name def __enter__(self): """ Открываем подключение с базой данных. """ self.conn = sqlite3.connect(self.db_name) return self.conn def __exit__(self, exc_type, exc_val, exc_tb): """ Закрываем подключение. """ self.conn.close() if exc_val: raise if __name__ == '__main__': db = 'test.db' with DataConn(db) as conn: cursor = conn.cursor() |
В данном коде мы создали класс, который берет путь к файлу базы данных SQLite Python. Метод __enter__ выполняется автоматически, он создает и возвращает объект связи базы данных. Теперь мы можем создать курсор для записи в базу данных или чтобы её запросить. Когда мы выходим из оператора with, метод __exit__ запускается, закрывая таким образом связь. Давайте попробуем создать контекстный менеджер при помощи другого метода.
Создание контекстного менеджера с использованием contextlib
В Python 2.5 добавили не только оператор with, но также модуль contextlib. Это позволяет нам создать контекстный менеджер, используя функцию модуля contextlib под названием contextmanager в качестве декоратора. Давайте попробуем создать контекстный менеджер который открывает и закрывает файл после проделанной в нем работе:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
from contextlib import contextmanager @contextmanager def file_open(path): try: f_obj = open(path, 'w') yield f_obj except OSError: print("We had an error!") finally: print('Closing file') f_obj.close() if __name__ == '__main__': with file_open('test.txt') as fobj: fobj.write('Testing context managers') |
Здесь мы просто импортируем contextmanager из contextlib и декорируем нашу функцию file_open с ним. Это позволяет нам вызвать file_open используя оператор with. В нашей функции мы открываем файл, отдаем его, чтобы функция calling могла использовать его. После того, как оператор закончит, контроль возвращается обратно к функции file_open, которая продолжает следовать по коду за вызываемым оператором. Это приводит оператор finally к исполнению, благодаря которому и закрывается файл. Если возникла ошибка OSError во время работы с файлом, она будет выявлена и оператор finally закроет обработчик файлов несмотря на это.
contextlib.closing()
Модуль contextlib содержит несколько полезных утилит. Первая – это класс closing, который закроет объект по завершению определенного блока кода. В документации Python есть пример кода, похожий на следующий:
1 2 3 4 5 6 7 8 |
from contextlib import contextmanager @contextmanager def closing(db): try: yield db.conn() finally: db.close() |
В целом, мы создаем закрывающую функцию, которая завернута в контекстный менеджер. Это эквивалент того, что делает класс closing. Но есть небольшая разница: вместо декоратора, мы можем использовать класс class в нашем операторе with. Давайте взглянем:
1 2 3 4 5 6 7 |
from contextlib import closing from urllib.request import urlopen with closing(urlopen('http://www.google.com')) as webpage: for line in webpage: # обрабатываем строку... pass |
В данном примере мы открыли страницу URL, но обернули её в наш класс closing. Это приведет к закрытию дескриптора веб-страницы, сразу после выхода из блока кода оператора with.
Есть вопросы по Python?
На нашем форуме вы можете задать любой вопрос и получить ответ от всего нашего сообщества!
Паблик VK
Одно из самых больших сообществ по Python в социальной сети ВК. Видео уроки и книги для вас!
contextlib.suppress(*exceptions)
Еще один полезный инструмент — класс suppress, который был добавлен в Python 3.4. Идея в том, что данная утилита контекстного менеджера может подавлять любое количество исключений. Скажем, нам нужно проигнорировать исключение FileNotFoundError. Если прописать следующий контекстный менеджер, то это не сработает:
1 2 3 |
with open('fauxfile.txt') as fobj: for line in fobj: print(line) |
1 2 3 |
Traceback (most recent call last): Python Shell, prompt 4, line 1 builtins.FileNotFoundError: [Errno 2] No such file or directory: 'fauxfile.txt' |
Как мы видим, этот контекстный менеджер не выполняет обработку данного исключения. Если вам нужно проигнорировать эту ошибку, лучше напишите следующий код:
1 2 3 4 5 6 |
from contextlib import suppress with suppress(FileNotFoundError): with open('fauxfile.txt') as fobj: for line in fobj: print(line) |
Здесь мы импортируем suppress и передаем его исключению FileNotFoundError. Если вы запустите этот код, вы увидите, что ничего не происходит, так как файл не существует, но и ошибка не возникает. Обратите внимание на то, что этот контекстный менеджер является реентрабельным, но об этом позже.
contextlib.redirect_stdout / redirect_stderr
Библиотека contextlib содержит несколько замечательных инструментов для перенаправления stdout и stderr, которые появились в Python 3.4 и 3.5 соответственно. До того, как эти инструменты появились, и когда вам нужно перенаправить stdout, вам нужно сделать что-то на подобии этого:
1 2 3 4 5 |
path = '/path/to/text.txt' with open(path, 'w') as fobj: sys.stdout = fobj help(sum) |
С модулем contextlib вы можете сделать следующее:
1 2 3 4 5 6 |
from contextlib import redirect_stdout path = '/path/to/text.txt' with open(path, 'w') as fobj: with redirect_stdout(fobj): help(redirect_stdout) |
В обоих примерах мы перенаправили stdout к файлу. Когда мы вызываем справку Python, вместо вывода в stdout, она сохраняется непосредственно в файле. Вы также можете перенаправить stdout в какой-нибудь буфер или текстовый инструмент управления из арсенала пользовательского интерфейса, вроде Tkinter или wxPython.
ExitStack
ExitStack – это контекстный менеджер, который позволит вам легко комбинировать другие контекстные менеджеры, а также функции очистки. Звучит немного запутанно, на первый взгляд, так что давайте рассмотрим простой пример из документации Python, с его помощью будет проще уловить суть:
1 2 3 4 5 6 |
from contextlib import ExitStack with ExitStack as stack: file_objects = [ stack.enter_context(open(fname)) for filename in filenames ] |
В общем и целом, данный код создает серию контекстных менеджеров внутри списка. ExitStack поддерживает стек регистрируемых колбеков, которые вызываются в обратом порядке когда экземпляр закрыт, что и происходит, когда мы выходим из части the оператора with. В документации Python существует великое множество метких примеров работы contextlib, где вы можете ознакомиться с такими темами как:
- Выявление исключений из методов __enter__
- Поддержки переменного количества контекстных менеджеров
- Замена любого применения try-finally
- И многое другое!
Я настоятельно рекомендую ознакомиться с этими темами, так как вы поймете, насколько эффективным и полезным может быть этот класс.
Реентерабельные контекстные менеджеры
Большая часть создаваемых вами контекстных менеджеров может быть написана только для использования с оператором with для одноразового применения. Вот пример:
1 2 3 4 5 6 7 8 9 10 11 |
from contextlib import contextmanager @contextmanager def single(): print('Yielding') yield print('Exiting context manager') context = single() with context: pass |
Результат:
1 2 |
Yielding Exiting context manager |
1 2 |
with context: pass |
1 2 3 4 5 |
Traceback (most recent call last): Python Shell, prompt 9, line 1 File "/usr/local/lib/python3.5/contextlib.py", line 61, in __enter__ raise RuntimeError("generator didn't yield") from None builtins.RuntimeError: generator didn't yield |
Здесь мы создали экземпляр контекстного менеджера и пытаемся запустить его дважды с оператором with. Второй запуск приводит к ошибке RuntimeError. Но что делать, если нам необходимо, чтобы контекстный менеджер запускался дважды? Для этой цели нам и нужен реентрабельный контекстный менеджер. Давайте используем менеджер redirect_stdout, который мы применяли ранее.
1 2 3 4 5 6 7 8 9 10 11 |
from contextlib import redirect_stdout from io import StringIO stream = StringIO() write_to_stream = redirect_stdout(stream) with write_to_stream: print('Write something to the stream') with write_to_stream: print('Write something else to stream') print(stream.getvalue()) |
Результат
1 2 |
Write something to the stream Write something else to stream |
Здесь мы создали вложенные контекстные менеджеры, которые оба пишут в StringIO, который является текстовым потоком в памяти. Причина, по которой это работает, а не приводит к ошибке RuntimeError, как было ранее в том, что redirect_stdout является реентрабельным и позволяет нам вызывать его дважды. Конечно, ситуации в реальной жизни могут быть заметно сложнее, когда мы работаем с большим количеством функций, которые вызывают друг друга. Пожалуйста, обратите также внимание на то, что контекстные менеджеры не обязательно являются защищенными от потоков. Обратитесь к документации, перед тем как использовать их в потоках во избежание путаницы.
Подведем итоги
Контекстный менеджер – крайне полезный и удобный инструмент, способный выручить во многих ситуациях. Я пользуюсь ими в своих автоматических тестах постоянно, для открытия и закрытия диалогов, например. Теперь вы можете использовать ряд встроенных в Python инструментов для создания собственных контекстных менеджеров. Убедитесь в том, что вы выделили достаточно времени для изучения документации Python о contextlib, так как в ней хранится очень много дополнительной полезной информации, которая не была рассмотрена в данной статье.
Являюсь администратором нескольких порталов по обучению языков программирования Python, Golang и Kotlin. В составе небольшой команды единомышленников, мы занимаемся популяризацией языков программирования на русскоязычную аудиторию. Большая часть статей была адаптирована нами на русский язык и распространяется бесплатно.
E-mail: vasile.buldumac@ati.utm.md
Образование
Universitatea Tehnică a Moldovei (utm.md)
- 2014 — 2018 Технический Университет Молдовы, ИТ-Инженер. Тема дипломной работы «Автоматизация покупки и продажи криптовалюты используя технический анализ»
- 2018 — 2020 Технический Университет Молдовы, Магистр, Магистерская диссертация «Идентификация человека в киберпространстве по фотографии лица»