Привет. В этом видео мы поговорим с вами о контекстных менеджерах.
С контекстными менеджерами вы уже работаете
и работали, когда открывали, например, файлы.
Мы с вами конкретно не останавливались на том, как это
происходит внутри, но вы знаете, что если использовать
контекстный менеджер with с открытием файла, вам не нужно
заботиться о том, чтобы его потом закрыть,
то есть контекстный менеджер делает это за вас. Вы открываете файл,
записываете открытый файл в переменную f,
записываете какие-то данные,
и потом он сам как-то закрывается, вам не нужно писать f close.
Контекстные менеджеры позволяют вам делать именно это.
Они позволяют определить поведение,
которое происходит в начале и в конце блока исполнения, блока with.
Часто бывает необходимо,
как, например, в случае с файлами, отрывать и закрывать какой-то ресурс
в обязательном порядке. Например, вам нужно
после открытия сокета его закрыть
или закрыть обязательно какое-то соединение.
Чтобы об этом не заботиться, об этом не помнить,
можно использовать контекстный менеджер.
Также, например, они используются при работе с транзакциями.
Вам нужно обязательно либо закончить транзакцию, либо ее откатить,
и вы можете определить контекстный менеджер, который будет вам
управлять поведением открытия и закрытия блока кода.
Чтобы определить свой контекстный менеджер,
как вы могли догадаться, нужно написать свой класс
с магическими методами.
Эти магические методы enter и exit,
которые как раз говорят о том, что происходит в начале и в конце
контекстного менеджера.
Давайте попробуем написать аналог контекстного менеджера
стандартного для открытия файлов, назовем его open file.
Обратите внимание, название класса с маленькой буквы,
потому что это контекстный менеджер, это не CamelCase.
Итак, у нас контекстный менеджер используется точно так же,
как и стандартный, мы вызываем open file,
в этот момент создается объект, то есть вызывается метод init,
и мы записываем в переменную класса f, открытый файл
с каким-то именем и открытый с каким-то mode'ом.
Отлично. Потом эта переменная f записывается из метода enter.
То есть из метода enter у нас возвращается что-то,
если нам нужно это потом записать
с помощью оператора as.
Мы можем ничего не возвращать из enter, и тогда у нас
нет смысла использовать as.
Что логично в методе exit у нас определяется поведение, которое
происходит при выходе из блока контекстного менеджера.
В данном случае нам нужно закрыть обязательно файл.
То есть когда у нас закончится этот блок, то есть где-то здесь,
у нас произойдет закрытие файла, и вам не нужно об этом заботиться
каждый раз, когда вы используете контекстный менеджер.
Итак, мы открыли файл и записали в него какую-то строчку.
Если попробовать прочитать этот файл, окажется, что она действительно там,
файл у нас открылся и закрылся сам.
Это очень удобно, и вы можете определять свое собственное поведение
при выходе из блока.
Еще одна важная особенность контекстных менеджеров - они позволяют
вам управлять исключениями, которые произошли внутри блока.
Мы можем эти исключения обрабатывать и определять какое-то поведение.
Например, мы можем определить контекстный менеджер
suppress exception, который будет работать с exception'ами,
которые произошли внутри.
Обратите внимание, в данном случае мы не используем as, оператор as,
поэтому нам не важно, что возвращается из enter'а,
просто его определили и написали return.
Могли написать просто pass.
Итак, у нас есть контекстный менеджер suppress exception, который принимает
exception, то есть exception type, класс exception'а.
Мы этот exception type записываем
и потом будем проверять,
произошло ли действительно это исключение или какое-то другое.
Поведение таково, что если
исключение произошло от того типа, который нам интересен,
мы делаем вид, что ничего не произошло.
То есть мы подавляем это исключение в suppress mode.
Мы просто выводим, что ничего не произошло, и возвращаем true.
Нужно обязательно вернуть true из exit'а при исключении, чтобы
воспроизведение кода продолжилось и exception не был выброшен.
Таким образом, мы можем поделить на 0
и exception засаппрессится, ничего не произойдет.
Часто бывает это очень полезно и удобно делать.
Что интересно, такой контекстный менеджер уже есть
в стандартной библиотеке в contextlib'е,
и вы можете его использовать и можете посмотреть,
как он на самом деле работает внутри и окажется,
что он работает примерно так же.
Давайте попробуем написать свой собственный контекстный менеджер
в качестве примера. Это будет контекстный менеджер, который
считает время, проведенное внутри него.
То есть у нас при открытии блока что-то произошло, и при закрытии мы
должны вывести время, которое произошло внутри
контекстного менеджера.
Давайте напишем наш класс, назовем его timer
и определим сразу методы enter
и exit, потому что именно они
говорят нам о том, что это контекстный менеджер.
Отлично. Как мы будем использовать наш класс?
Мы будем использовать оператор with timer.
Потом что-то должно случиться внутри контекстного менеджера,
и мы должны вывести время, в которое
это все дело происходило.
Итак,
чтобы нам считать время, которое прошло внутри контекстного менеджера,
нам нужно где-то завести переменную, которая берет начало,
собственно, которая и записывает время в начале выполнения операции.
Происходит это, конечно, в методе init, потому что у нас
создается объект класса.
И давайте с помощью
встроенного модуля time
сохраним в переменную start текущее время.
То есть в момент инициализации, то есть вот здесь вот, когда у нас
вызвался таймер, у нас запишется текущее время.
В enter'е мы просто вернем ничего,
и в exit'е нам интересно вывести время, которое прошло с момента начала.
Для этого мы просто напишем time,
time и на self.start.
То есть выведем время, которое как раз прошло.
И чтобы у нас контекстный менеджер разрешился не мгновенно,
давайте напишем здесь time.sleep
и будем спать в течение одной секунды.
Отлично.
Да, у нас действительно вывелось время,
давайте сделаем это немного посимпатичнее,
как-то так,
да.
Давайте попробуем модифицировать немного
контекстный менеджер, чтобы что-то возвращать из return'а, чтобы
можно было, например, нам смотреть, сколько времени прошло
на текущий момент, если у нас
несколько, допустим, операций sleep.
Здесь мы хотим, например, as t
и вывести current_time
и сделать еще один time.sleep.
Что нам для этого нужно сделать? Например, мы можем
вернуть самого себя в enter'е и определить метод
current_time,
который будет возвращать время, прошедшее с начала выполнения.
Делает он точно то же самое,
time.time − self.start.
Ну и здесь можно тоже заменить тогда
на self.current_time.
Давайте