Пишем контекстные менеджеры в python
Контекстный менеджер в python — это объект, определяющий, что должно быть сделано «До» и «После» тела with выражения. Чаще всего контекстные менеджеры используют для управления ресурсами, получение и освобождение. Тема хорошо объясняется на следующем, довольно распространённом, примере с функцией open.
Допустим, нам необходимо открыть, прочитать и закрыть файл. Причём закрытие файла должно быть гарантированным:
В случае возникновения какого-либо исключения в промежутке между открытием и закрытием файла .close() не отработает — это утечка файлового дескриптора. Этого можно избежать, применив finally:
Теперь то же самое, но в виде контекстного менеджера:
Лаконичнее и чище.
Как написать свой:
Если замечаете, что в вашем коде встречается подобный повторяющийся try…finally, то можно написать свой контекстный менеджер. Например, вам нужно исполнять sql запрос предварительно приконнектившись к БД, а по завершении обязательно закрыть коннект(даже если по пути вылезло исключение). Далее мы такой менеджер и напишем несколькими способами. Для примера возьмём sqlite3.
- способ с __exit__ и __enter__:
Данный способ заключается в написании класса, в котором необходимо определить специальные, указанные выше, методы:
И теперь можно использовать так:
- Способ: декоратор contextlib.contextmanager:
contextlib — это встроенная библиотека, которая помогает писать понятные контекстные менеджеры. Ниже реализация аналога примера из пункта 1:
Обратите внимание на использование yield. По сути всё, что в блоке try — это то же самое, что и __enter__ метод, а в finally это тот самый клинап ресурсов — __exit__.
- Способ contextlib.closing:
В этом способе вам вообще ничего не придётся писать, при условии, что клинап метод у вас называются close().
Так, в sqlite3 есть метод .close(), который отвечает за закрытие коннекта к БД, а значит мы можем спокойно использовать данный способ:
Тема контекстных менеджеров достаточно обширная и рекомендуется, как минимум, самостоятельно поизучать какие ещё есть встроенные контекстные менеджеры(включая те, что есть в библиотеке contextlib).
Контекстный менеджер в Python
Менеджер контекста — это объект, определяющий контекст выполнения в операторе with.
Давайте начнем с простого примера, чтобы понять концепцию менеджера контекста.
Предположим, что у yас есть файл data.txt, в котором содержится целое число 100.
Теперь напишем программу, которая читает файл data.txt, преобразует его содержимое в число и выводит результат на стандартный вывод:
Код прост и понятен.
Однако что если в файле data.txt будут содержаться данные, которые не нельзя преобразовать в целое число? В таком случае возникнет исключение ValueError.
Например, если в файл data.txt записать строку ‘100’ вместо числа 100, вы получите следующую ошибку:
Из-за этого исключения Python может не закрыть файл должным образом.
Чтобы исправить такое поведение, можно воспользоваться оператором try. except. finally :
Поскольку код в блоке finally всегда выполняется, программа всегда будет правильно закрывать файл.
Это решение работает как надо, но оно слишком многословное.
Поэтому в Python есть способ автоматически закрыть файл после завершения его обработки — и он менее многословный.
Здесь в игру вступают менеджеры контекста.
Ниже показано, как использовать менеджер контекста для обработки файла data.txt:
В этом примере мы используем функцию open() с оператором with . После блока with Python автоматически закроется.
Оператор with
Синтаксис
Как это работает
- Когда Python встречает оператор with , он создает новый контекст. При желании контекст может возвращать объект.
- После блока with Python автоматически очищает контекст.
- Область видимости ctx имеет ту же область видимости, что и оператор with . Это означает, что вы можете обращаться к ctx как внутри оператора with , так и после него.
Ниже показано, как получить доступ к переменной f после оператора with :
Протокол контекстного менеджера
Контекстные менеджеры Python работают на основе протокола контекстного менеджера.
Протокол менеджера контекста включает следующие методы:
- __enter__() — устанавливает контекст и, по желанию, возвращать некоторый объект.
- __exit__() — очищает объект.
Если вы хотите, чтобы класс поддерживал протокол контекстного менеджера, вам необходимо реализовать эти два метода.
Предположим, что у нас есть некий класс ContextManager , поддерживающий протокол контекстного менеджера.
Вот, как можно использовать этот класс:
Когда вы используете класс ContextManager с оператором with , Python неявно создает экземпляр класса — instance — и автоматически вызывает метод __enter__() на этом экземпляре.
Метод __enter__() может по желанию возвращать объект. Если это так, Python присваивает возвращаемый объект ctx .
Обратите внимание, что ctx ссылается на объект, возвращаемый методом __enter__() . Он не ссылается на экземпляр класса ContextManager .
Если внутри блока with или после блока with возникает исключение, Python вызывает метод __exit__() на объекте экземпляра.
Функционально оператор with эквивалентен конструкции try. finally :
Метод __enter__()
В методе __enter__() можно выполнить необходимые действия по настройке контекста.
При необходимости вы можете вернуть объект из метода __enter__() .
Метод __exit__()
Python всегда выполняет метод __exit__() , даже если в блоке with возникает исключение.
Метод __exit__() принимает три аргумента: тип исключения, значение исключения и объект трассировки. Все эти аргументы будут равны None , если исключение не произошло.
Метод __exit__() возвращает логическое значение: True или False .
Если возвращаемое значение равно True , Python заглушит исключение.
Как можно использовать контекстный менеджер
В этой статье мы уже выяснили, что контекстный менеджер можно использовать для автоматического открытия и закрытия файлов.
Давайте разберемся, в каких случаях еще можно использовать контекстный менеджер. Вот некоторые из них:
1) Закрытие — открытие
Если вы хотите открывать и закрывать ресурс автоматически, вы можете использовать контекстный менеджер.
Например, вы можете открыть сокет и закрыть его с помощью контекстного менеджера.
2) Заблокировать — разблокировать
Контекстные менеджеры помогут вам более эффективно управлять блокировками объектов.
3) Запустить — остановить
Контекстные менеджеры помогут вам работать со сценариями, требующими запуска и остановки.
Например, можно использовать контекстный менеджер для запуска таймера и его автоматической остановки.
4) Изменить — сбросить
Контекстные менеджеры могут работать со сценариями изменения и сброса.
Например, вашему приложению необходимо подключиться к нескольким источникам данных. И у него есть соединение по умолчанию.
Вот алгоритм для подключения к другому источнику данных:
- Используйте контекстный менеджер для изменения соединения по умолчанию на новое.
- Работайте с новым соединением.
- После завершения работы с новым соединением верните его обратно к соединению по умолчанию.
Создание протокола контекстного менеджера
Ниже показана простая реализация функции open() с использованием протокола контекстного менеджера:
Как это работает
- Инициализируем имя файла и режим в методе __init__() .
- Открываем файл в методе __enter__() и возвращаем объект файла.
- Закрываем файл, если он открыт, в методе __exit__() .
Реализации шаблона запуска и остановки с помощью контекстного менеджера
Давайте создадим класс Timer , который поддерживает протокол контекстного менеджера:
Как это работает
- Импортируем perf_counter из модуля time .
- Запускаем таймер в методе __enter__() .
- Останавливаем таймер в методе __exit__() и верните прошедшее время.
Теперь вы можете использовать класс Timer для измерения времени, необходимого для вычисления числа Фибоначчи, равного 1000, один миллион раз:
Введение
![]()
В Python при работе с файлами наиболее распространённой функция open() , создающая объект типа файл, который в зависимости от ситуации позволяет читать или записывать данные. Мы используем функцию open() почти всегда с оператором with , согласно официальному справочнику и онлайн руководствам. Основная форма показана ниже.
Запустив код, видим, что файл с именем hello.txt был создан в рабочей директории. Чтобы проверить, что строка Hello World! записана в файл, откроем его:
Мы можем прочитать файл, открыв его с помощью функции open() , задав режим чтения ( r ).
Автоматическое закрытие файла
Возможно, многие знают, почему для открытия файла здесь мы применяем оператор with . Те, кто не знает, просмотрите сперва следующий код:
В коде выше мы изменили файл, добавив к нему дополнительную строку (заметьте, что для добавления мы используем режим a ). Когда мы закончили с оператором with , то обнаружили, что файл был закрыт, хотя мы не вызывали метод close() явно для файлового объекта. И это в точности то, что делает оператор with — автоматически закрывает файл при выходе.
И что же в этом такого? Рассмотрим следующий тривиальный пример:
Сначала мы изменили файл, добавив некоторые новые данные, но забыли закрыть его после выполнения операции. Когда мы снова читаем файл, мы не видим изменения, которые вроде бы добавили, что может спровоцировать ошибки в коде. Если же мы используем оператор with , каждая операция с файлом очищается Python, автоматически закрывая файл. Что более важно, мы можем производить более сложные операции с файлом, некоторые из которых могут включать исключения, которые остановят работу программы. При таких возможных сценариях у нас всё ещё будет шанс закрыть файл безопасно и автоматически с помощью with .
Менеджеры контекста
В более широком смысле оператор with для открытия файла — это пример использования менеджера контекста.
Что такое менеджер контекста? Это объект Python, который выполняет за вас рутинную работу, когда вы используете определённые ресурсы. В частности, менеджер контекста задаёт временный контекст и ликвидирует его после выполнения операций.
Если говорить об операции открытия файла, работу менеджера контекста можно продемонстрировать с помощью операторов try , except и finally . Рассмотрим следующий псевдокод для возможной реализации оператора with :
Менеджер контекста открывает файл и создаёт объект, с которым дальше будет производиться работа. Когда мы завершим операцию и любые исключения, выброшенные в процессе выполнения операции, менеджер контекста закроет файл. Поскольку файлы являются совместно используемыми ресурсами и находятся в вашей ответственности (задачу управления ими помогают решать менеджеры контекста), критически важно, чтобы вы освободили их после того, как выполните операции, чтобы другие процессы получили к ним доступ.
Использование с обработкой сообщений
Как мы уже обсудили, оператор with лучше всего применять в ситуациях, когда нам необходимо работать с чем-то коллективно используемым. Один из ярких примеров — работа с данными в многопоточных проектах. Вам наверняка известно, к какому беспорядку приводит ситуация, когда несколько потоков получают доступ к одному и тому же массиву данных: одна операция добавляет данные, в то время как другая читает старые данные в другой файловой операции.
Например, один поток пытается добавить данные в словарь, а другой в то же время пытается выполнять итерирование словаря. Всё слишком быстро выходит из-под контроля. Для решения этой проблемы мы можем использовать блокировку потока, чтобы уменьшить беспорядок в данных. Важно заметить, что, поскольку мы хотим получить полный контроль над ресурсами на временной основе, лучше всего применить оператор with . Взгляните на следующий пример:
Как видим, оператор with может кардинально улучшить осмысленность вашего кода. Что ещё более важно, блокировка снимается автоматически, когда операция завершается с оператором with . Без использования менеджера контекста, то есть оператора with , нам придётся управлять этими ресурсами вручную и очень осторожно. Если мы забудем снять блокировку, наша программа столкнётся с неожиданными проблемами.
Протокол менеджера контекста
Создавать менеджеры контекста можно, чтобы самостоятельно управлять некоторыми ресурсами. Одним из способов создания является реализация методов для протокола менеджера контекста. Можете представить это себе как утиную типизацию — мы просто определим магические методы __enter__ и __exit__ без формального согласования протокола или реализации интерфейса, как это можно сделать и в других языках программирования. Следующий код демонстрирует эту концепцию:
Как показано выше, мы просто определили класс, в котором реализованы методы __enter__ и __exit__ , способные управлять контекстом за нас. С синтаксической точки зрения, мы можем использовать этот класс в операторе with , как в строке 12. Выведенный текст показывает нам порядок, в котором эти операции хорошо координируются. В частности, созданный экземпляр (строка 15) вызовет метод __enter__ (строка 16) для запуска контекста, затем мы сами выполняем операции (строка 17), и, наконец, менеджер контекста выйдет из управления, вызвав метод __exit__ .
Модуль contextlib
Вы обнаружите, что самостоятельная реализация специальных методов __enter__ и __exit__ для создания менеджера контекста может оказаться утомительной. С модулем contextlib в стандартной библиотеке Python намного проще управлять контекстом. Полный обзор этого модуля выходит за рамки данной статьи, я просто расскажу о конкретном методе для создания менеджера контекста. Но сперва давайте немного вернёмся назад, потому что здесь уместно упомянуть декораторы.
Декораторы — это функции, которые изменяют поведение других функций, не затрагивая их ключевые функциональности. Другими словами, декорированная функция выполнит то, что ей и положено, но декоратор придаст ей дополнительные действия. Рассмотрим концепцию декоратора на коротком примере:
Для декораторов вам просто нужно создать функцию, которая принимает другую функцию в качестве входных данных. Декорация — это операция, определённая в функции-декораторе. В данном случае мы просто будем делать записи до и после вызова функции. Чтобы использовать декоратор, напишем имя функции с префиксом @ . Можно сказать, что вызов декорированной функции (строка 15) успешно привёл к дополнительному логированию до и после вызова функции.
Теперь, разобравшись с декораторами, рассмотрим пример использования модуля contextlib , который поможет нам с управлением контекстом в следующем фрагменте кода:
Используем функцию-декоратор contextmanager для декорирования функции context_manager_example . В теле функции вы можете заметить нечто необычное — ключевое слово yield . Вы уже должны были встретиться с этим словом, когда изучали генераторы — итераторы, отображающие элементы, когда их об этом просят (так называемое ленивое вычисление). В этих случаях “ yield ” означает “продукт”.
Однако в нашем случае это слово означает “уступать”. В частности как только менеджер контекста (декорированная функция context_manager_example ) завершает настройку, она уступает выполнение коду с оператором with . После завершения операции контроль возвращается к функции. Важно, что yield в Python специально обрабатывается, поэтому он запускается в том месте, где был запущен. Вот почему функция print , следующая за ключевым словом yield , вызывается только один раз сразу после завершения операций в операторе with .
Выводы
В этой статье мы рассмотрели концепцию менеджеров контекста на примере операции с файлами, включающей оператор with . Мы усвоили, что именно менеджер контекста помог нам выполнить вспомогательную работу, закрыв файл.
В более широком смысле менеджеры контекста полезны для управления ресурсами конкретной программы или других программ на компьютере, предназначенных для совместного использования. Менеджеры контекста помогают ответственно управлять получением и освобождением этих совместных ресурсов. Мы также рассмотрели, как можно переопределять методы __enter__ и __exit__ для создания собственных классов менеджера контекста. Кроме того, рассмотрели альтернативный метод применения модуля contextlib для создания менеджеров контекста с использованием декораторов.
Однако мы осветили не все вопросы. Например, сигнатура функции метода __exit__ имеет другие параметры, которые мы не реализовали, например, обработку исключений. Полная реализация этих параметров должна оцениваться в индивидуальном порядке.
Некоторые возможности Python о которых вы возможно не знали
Я очень полюбил Python после того, как прочитал книгу Марка Лутца «Изучаем Python». Язык очень красив, на нем приятно писать и выражать собственные идеи. Большое количество интерпретаторов и компиляторов, расширений, модулей и фреймворков говорит о том, что сообщество очень активно и язык развивается. В процессе изучения языка у меня появилось много вопросов, которые я тщательно гуглил и старался понять каждую непонятую мной конструкцию. Об этом мы и поговорим с вами в этой статье, статья ориентирована на начинающего Python разработчика.
Немного о терминах
Начну пожалуй с терминов, которые часто путают начинающих Python программистов.
List comprehensions или генераторы списков возвращают список. Я всегда путал генераторы списков и выражения — генераторы (но не генераторы выражений!). Согласитесь, по русский звучит очень похоже. Выражения — генераторы это generator expressions, специальные выражения, которые возвращают итератор, а не список. Давайте сравним:
Это две совершенно разные конструкции. Первый возвращает генератор (то есть итератор), второй обычный список.
Generators или генераторы это специальные функции, которые возвращают итератор. Что бы получить генератор нужно возвратить функции значение через yield:
Кстати, в Python 3.3 появилась новая конструкция yield from. Совместное использование yield и for используется настолько часто, что эти две конструкции решили объединить.
Что такое контекстные менеджеры и для чего они нужны?
Контекстные менеджеры это специальные конструкции, которые представляют из себя блоки кода, заключенные в инструкцию with. Инструкция with создает блок используя протокол контекстного менеджера, о котором мы поговорим далее в этой статье. Простейшей функцией, использующей данный протокол является функция open(). Каждый раз, как мы открываем файл нам необходимо его закрыть, что бы вытолкнуть выходные данные на диск (на самом деле Python вызывает метод close() автоматически, но явное его использование является хорошим тоном). Например:
Что бы каждый раз не вызывать метод close() мы можем воспользоваться контекстным менеджером функции open(), который автоматически закроет файл после выхода из блока:
Здесь нам не нужно каждый раз вызывать метод close, что бы вытолкнуть данные в файл. Из этого следует, что контекстный менеджер используется для выполнения каких либо действий до входа в блок и после выхода из него. Но функциональность контекстных менеджеров на этом не заканчивается. Во многих языках программирования для подобных задач используются деструкторы. Но в Python если объект используется где то еще то нет гарантии, что деструктор будет вызван, так как метод __del__ вызывается только в том случае, если все ссылки на объект были исчерпаны:
Решим эту задачу через контекстные менеджеры:
Теперь попробуем вызвать менеджер контекста:
Мы увидели, что произошел гарантированный выход из блока после выполнения нашего кода.
Протокол контекстного менеджера
Мы уже кратко рассмотрели протокол контекстного менеджера написав небольшой класс Hello. Давайте теперь разберемся в протоколе более подробно. Что бы объект стал контекстным менеджером в его класс обязательно нужно включить два метода: __enter__ и __exit__. Первый метод выполняется до входа в блок. Методу можно возвратить текущий экземпляр класса, что бы к нему можно было обращаться через инструкцию as.
Метод __exit__ выполняется после выхода из блока with, и он содержит три параметра — exp_type, exp_value и exp_tr. Контекстный менеджер может вылавливать исключения, которые были возбуждены в блоке with. Мы можем вылавливать только нужные нам исключения или подавлять ненужные.
Переменная exp_type содержит в себе класс исключения, которое было возбуждено, exp_value — сообщение исключения. В примере мы закрываем файл и подавляем исключение IOError посредством возврата True методу __exit__. Все остальные исключения в блоке мы разрешаем. Как только наш код подходит к концу и блок заканчивается вызывается метод self.fp.close(), не зависимо от того, какое исключение было возбуждено. Кстати, внутри блока with можно подавлять и такие исключения как NameError, SyntaxError, но этого делать не стоит.
Протоколы контекстных менеджеров очень просты в использовании, но для обычных задач есть еще более простой способ, который поставляется вместе со стандартной библиотекой питона. Далее мы рассмотрим пакет contextlib.
Пакет contextlib
Создание контекстных менеджеров традиционным способом, то есть написанием классов с методами __enter__ и __exit__ не одна из сложных задач. Но для тривиального кода написание подобных классов требует больше возьни. Для этих целей был придуман декоратор contextmanager(), входящий в состав пакета contextlib. Используя декоратор contextmanager() мы можем из обычной функции сделать контекстный менеджер:
Проверим работоспособность кода:
Попробуем возбудить исключение внутри блока.
Как видно из примера, реализация с использованием классов практически ничем не отличается по функциональности от реализации с использованием декоратора contextmanager(), но использование декоратора намного упрощает наш код.
Еще один интересный пример использования декоратора contextmanager():
Похоже на блоки в руби не так ли?
И напоследок поговорим о вложенных контекстах. Вложенные контексты позволяют управлять несколькими контекстами одновременно. Например:
вход в контекст first
вход в контекст second
внутри блока first second
выход из контекста second
выход из контекста first
Аналогичный код без использования функции nested:
Этот код хоть и похож на предыдущий, в некоторых ситуациях он будет работать не так как нам хотелось бы. Объекты context(‘first’) и context(‘second’) вызываются до входа в блок, поэтому мы не сможем перехватывать исключения, которые были возбуждены в этих объектах. Согласитесь, первый вариант намного компактнее и выглядит красивее. А вот в Python 2.7 и 3.1 функция nested устарела и была добавлена новая синтаксическая конструкция для вложенных контекстов:
range и xrange в Python 2.7 и Python 3
Известно, что Python 2.7 range возвращает список. Думаю все согласятся, что хранить большие объемы данных в памяти нецелесообразно, поэтому мы используем функцию xrange, возвращающий объект xrange который ведет себя почти так же как и список, но не хранит в памяти все выдаваемые элементы. Но меня немного удивило поведение xrange в Python 2.x, когда функции передаются большие значения. Давайте посмотрим на пример:
Python нам говорит о том, что int слишком длинный и он не может быть переконвертирован в C long. Оказывается у Python 2.x есть ограничения на целое число, в этом мы можем убедиться просмотрев константу sys.maxsize:
Вот оно максимальное значение целого числа:
Python аккуратно переконвертировал наше число в long int. Не удивляйтесь, если xrange в Python 2.x будет вести себя иначе при больших значениях.
В Python 3.3 целое число может быть бесконечно большим, давайте проверим:
Конвертирование в long int не произошло. Вот еще пример:
Не очевидное поведение некоторых конструкций
Думаю все согласятся, что простота питона заключена не в легкости его изучении, а в простоте самого языка. Питон красив, гибок и на нем можно писать не только в объектно ориентированном стиле, но и в функциональном. Но о поведении некоторых конструкций, который на первый взгляд кажутся странными необходимо знать. Для начала рассмотрим первый пример.
Каков будет результат выполнения данной конструкции? Неподготовленный разработчик сообщит о результате: [[‘a’], [b’], [c’]]. Но на самом деле мы получаем:
Почему в каждом списке результат дублируется? Дело в том, что оператор умножения создает ссылки внутри нашего списка на один и тот же список. В этом легко убедиться немного дополнив наш пример:
В первом случае все нормально и ссылки на списки разные, а во втором примере мы ссылаемся на один и тот же объект. Из этого следует, что изменение в первом списке повлечет за собой изменение в последующих, так что будьте внимательны.
Второй пример уже рассматривался на хабре, но мне захотелось включить его в статью. Посмотрим на lambda — функцию, которую мы будет прогонять через цикл for, и помещать каждую функцию в словарь:
В пределах lambda функции переменная i замыкается и как бы создается экземпляр еще одной переменной i в блоке lambda — функции, которая является ссылкой на переменную i в цикле for. Каждый раз когда счетчик цикла for меняется, меняются и значения во всех lambda функциях, поэтому мы получаем значение i-1 во всех функциях. Исправить это легко, явно передав lambda функции в качестве первого параметра значение по умолчанию — переменную i: