Инструкция with в языке Python часто заводит в тупик тех, кто только начинает своё знакомство с этим языком. Менеджер контекста, создаваемый инструкцией with, таит в себе невиданную мощь и очень полезный функционал, позволяющий безопасно использовать ресурсы, требуемые получения и обязательного освобождения(завершения). Однако, творческий подход позволяет с удобством использовать этот инструмент и в других областях.
Как это работает?
Рассмотрим задачу записи данных в последовательный порт. Для решения этой задачи с применением контекста используют следующий код:
import serial
with serial.Serial(port='COM3') as s:
s.write('Привет')
s.write('Как дела')
Здесь мы определяем псевдоним s и используем его в контексте serial. В теле контекста может располагаться любой блок кода, в т.ч и другие контексты. Применение менеджера контекста позволяет гарантировать, что ресурс Serial будет закрыт, что удобно, например, в многопоточных приложениях, где ресурс надо быстро освобождать для возможности использования другими потоками. Или банальная ошибка программиста – забыть закрыть метод, тоже решается использованием менеджера контекста.
Зачем это нужно?
В приведённом выше примере вам не нужно вызывать функции open и close объекта serial, потому что их за вас вызовет менеджер контекста. То есть, вам не нужно беспокоится о закрытии порта после выполнения некоторых действий – это сделается автоматически.
Казалось бы совсем не сложно контролировать процесс освобождения ресурса самостоятельно, написав следующее:
import serial
s = serial.Serial(port='COM3')
s.open()
s.write('Привет')
s.write('Как дела?')
s.close()
Однако, есть тут один нюанс. Если вдруг где-то между открытием и закрытием порта произойдёт исключение (exception), то закрытие ресурса не гарантируется!
Конечно, вы можете избежать такой ситуации и модифицировать код следующим образом:
import serial
s = serial.Serial(port='COM3')
s.open()
try:
s.write('Привет')
s.write('Как дела?')
finally:
s.close()
И да, этот код будет абсолютно функционально эквивалентен применению менеджера контекста, он также будет безопасен, но вы можете сделать его проще и лаконичнее с помощью with.
Главная ценность инструкции with это возможность абстрагироваться от функциональности распространенных шаблонов управления ресурсов, упрощая работу с ними.
Как это использовать в собственных разработках?
Возникает закономерный вопрос: “Как инструкция with узнает, какой метод вызывать при открытии контекста и при его завершении”?
Для этого используются “волшебные” методы __enter__ и __exit__ или декоратор, созданный с помощью модуля contextlib. Рассмотрим оба случая.
Собственный класс
Метод __enter__ выполняется при входе в контекст, а метод __exit__ при выходе из него. Кроме безопасного доступа к ресурсам, этот функционал можно использовать для аккуратной реализации не связанных с ресурсами вещей, например, оформления вывода текста.
Давайте разберем на примере: напишем программу, которая будет решать творческую задачу через контексты: формировать маркированный список с разной степенью вложенности.
class list:
def __init__(self):
self.level = 0
def __enter__(self):
self.level += 1
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.level -= 1
def write(self, text):
print(' ' * self.level + '* ' + text)
with list() as l:
l.write("Фрукты")
with l:
l.write("Бананы")
l.write("Ананасы")
l.write("Ягоды")
with l:
l.write("Черника")
l.write("Малина")
И в результате выполнения программы мы получим следующий отформатированный список с двумя уровнями вложенности:
* Фрукты
* Бананы
* Ананасы
* Ягоды
* Черника
* Малина
Декоратор
Декоратор можно использовать когда создавать целый класс накладно, а вот одна функция вас вполне бы устроила. Давайте напишем функцию, которая предоставляет возможность безопасного использования последовательного порта: получая и освобождая ресурс. В помощь нам приходит библиотека contextlib
from contextlib import contextmanager
import serial
@contextmanager
def comOpen(portname):
ser = serial.Serial()
try:
ser.setPort(portname)
yield ser
except serial.serialutil.SerialException:
print("Ошибочка вышла!")
finally:
print('Закрываем порт')
ser.close()
with comOpen('COM3') as s:
s.write('Hello!')
И если мы выберем заранее неверный порт, то увидим на экране вывода сообщение:
Ошибочка вышла!
Закрываем порт
Реализация удалась! Менеджер контекста работает!
Резюме
- Менеджер контекста определяется инструкцией with и позволяет полностью абстрагироваться от управления ресурсами и гарантировать их освобождение при выходе из контекста
- Вы можете создавать собственные объекты, которые можно применять с менеджерами контекста
- Используя вложенность контекстов и творческих подход, можно использовать их для решения задач не связанных с владением и управлением ресурсами. Например, реализовать форматирование текста в виде список с разными уровнями вложенности