{ "cells": [ { "cell_type": "markdown", "id": "e5872b4b", "metadata": {}, "source": [ "# Контекстные менеджеры #" ] }, { "cell_type": "markdown", "id": "2219c4bc", "metadata": {}, "source": [ "В этой лекции мы поговорим с вами о контекстных менеджерах. С контекстными менеджерами вы уже работаете и работали, когда открывали, например, файлы. Мы с вами конкретно не останавливались на том, как это происходит внутри, но вы знаете, что если использовать контекстный менеджер `with` с открытием файла, вам не нужно заботиться о том, чтобы его потом закрыть, то есть контекстный менеджер делает это за вас. Вы открываете файл, записываете открытый файл в переменную `f`, записываете какие-то данные, и потом он сам как-то закрывается, вам не нужно писать `f.close()`." ] }, { "cell_type": "code", "execution_count": 1, "id": "1673e912", "metadata": {}, "outputs": [], "source": [ "with open(\"access_log.log\", \"a\") as f:\n", " f.write(\"New Access\")" ] }, { "cell_type": "code", "execution_count": 2, "id": "daa038dd", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "New Access\n" ] } ], "source": [ "! type access_log.log" ] }, { "cell_type": "markdown", "id": "c19a4d03", "metadata": {}, "source": [ "Контекстные менеджеры позволяют вам делать именно это. Они позволяют определить поведение, которое происходит в начале и в конце блока исполнения, блока `with`. Часто бывает необходимо, как, например, в случае с файлами, отрывать и закрывать какой-то ресурс в обязательном порядке." ] }, { "cell_type": "markdown", "id": "d671994d", "metadata": {}, "source": [ "Например, вам нужно после открытия сокета его закрыть или закрыть обязательно какое-то соединение. Чтобы об этом не заботиться, об этом не помнить, можно использовать контекстный менеджер. Также, например, они используются при работе с транзакциями. Вам нужно обязательно либо закончить транзакцию, либо ее откатить, и вы можете определить контекстный менеджер, который будет вам управлять поведением открытия и закрытия блока кода. Чтобы определить свой контекстный менеджер, как вы могли догадаться, нужно написать свой класс с магическими методами. Эти магические методы `__enter__` и `__exit__`, которые как раз говорят о том, что происходит в начале и в конце контекстного менеджера. Давайте попробуем написать аналог контекстного менеджера стандартного для открытия файлов, назовем его `open_file`. Обратите внимание, название класса с маленькой буквы, потому что это контекстный менеджер, это не `CamelCase`." ] }, { "cell_type": "code", "execution_count": 3, "id": "8b871709", "metadata": {}, "outputs": [], "source": [ "class open_file:\n", " def __init__(self, filename, mode):\n", " self.f = open(filename, mode)\n", " \n", " def __enter__(self):\n", " return self.f\n", " \n", " def __exit__(self, *args):\n", " self.f.close()\n" ] }, { "cell_type": "markdown", "id": "43d49b39", "metadata": {}, "source": [ "Итак, у нас контекстный менеджер используется точно так же, как и стандартный, мы вызываем `open_file`, в этот момент создается объект, то есть вызывается метод `__init__`, и мы записываем в переменную класса `f`, открытый файл с каким-то именем и открытый с каким-то `mode`'ом. Отлично. Потом эта переменная `f` записывается из метода `__enter__`. То есть из метода `__enter__` у нас возвращается что-то, если нам нужно это потом записать с помощью оператора `as`. Мы можем ничего не возвращать из `__enter__`, и тогда у нас нет смысла использовать `as`. Что логично в методе `__exit__` у нас определяется поведение, которое происходит при выходе из блока контекстного менеджера. В данном случае нам нужно закрыть обязательно файл. То есть когда у нас закончится этот блок, то есть где-то здесь, у нас произойдет закрытие файла, и вам не нужно об этом заботиться каждый раз, когда вы используете контекстный менеджер.\n", "\n", "Итак, мы открыли файл и записали в него какую-то строчку. Если попробовать прочитать этот файл, окажется, что она действительно там, файл у нас открылся и закрылся сам." ] }, { "cell_type": "code", "execution_count": 4, "id": "3e6d969b", "metadata": {}, "outputs": [], "source": [ "with open_file(\"test.log\", \"w\") as f:\n", " f.write(\"Inside `open_file` context manager\")" ] }, { "cell_type": "code", "execution_count": 5, "id": "a983869d", "metadata": { "scrolled": true }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "['Inside `open_file` context manager']\n" ] } ], "source": [ "with open_file(\"test.log\", \"r\") as f:\n", " print(f.readlines())" ] }, { "cell_type": "markdown", "id": "da15917a", "metadata": {}, "source": [ "Это очень удобно, и вы можете определять свое собственное поведение при выходе из блока. Еще одна важная особенность контекстных менеджеров - они позволяют вам управлять исключениями, которые произошли внутри блока. Мы можем эти исключения обрабатывать и определять какое-то поведение. Например, мы можем определить контекстный менеджер `suppress_exception`, который будет работать с `exception`'ами, которые произошли внутри." ] }, { "cell_type": "code", "execution_count": 6, "id": "7ef93a74", "metadata": {}, "outputs": [], "source": [ "class suppress_exception:\n", " def __init__(self, exc_type):\n", " self.exc_type = exc_type\n", " \n", " def __enter__(self):\n", " return\n", " \n", " def __exit__(self, exc_type, exc_value, traceback):\n", " if exc_type == self.exc_type:\n", " print(\"Nothing happend.\")\n", " \n", " return True" ] }, { "cell_type": "markdown", "id": "c7eeca4c", "metadata": {}, "source": [ "Обратите внимание, в данном случае мы не используем `as`, оператор `as`, поэтому нам не важно, что возвращается из `enter`'а, просто его определили и написали `return`. Могли написать просто `pass`. Итак, у нас есть контекстный менеджер `suppress_exception`, который принимает `exception`, то есть `exc_type`, класс `exception`'а. Мы этот `exc_type` записываем и потом будем проверять, произошло ли действительно это исключение или какое-то другое. Поведение таково, что если исключение произошло от того типа, который нам интересен, мы делаем вид, что ничего не произошло. То есть мы подавляем это исключение в `suppress mode`. Мы просто выводим, что ничего не произошло, и возвращаем `true`. Нужно обязательно вернуть `true` из `exit`'а при исключении, чтобы воспроизведение кода продолжилось и `exception` не был выброшен. Таким образом, мы можем поделить на `0` и `exception` засаппрессится, ничего не произойдет." ] }, { "cell_type": "code", "execution_count": 7, "id": "2fe27132", "metadata": { "scrolled": true }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Nothing happend.\n" ] } ], "source": [ "with suppress_exception(ZeroDivisionError):\n", " really_big_number = 1 / 0" ] }, { "cell_type": "markdown", "id": "fc919dcd", "metadata": {}, "source": [ "Часто бывает это очень полезно и удобно делать. Что интересно, такой контекстный менеджер уже есть в стандартной библиотеке в `contextlib`'е, и вы можете его использовать и можете посмотреть, как он на самом деле работает внутри и окажется, что он работает примерно так же." ] }, { "cell_type": "code", "execution_count": 8, "id": "73c3ecb9", "metadata": {}, "outputs": [], "source": [ "import contextlib\n", "\n", "with contextlib.suppress(ValueError):\n", " raise ValueError" ] }, { "cell_type": "markdown", "id": "08097626", "metadata": {}, "source": [ "Давайте попробуем написать свой собственный контекстный менеджер в качестве примера. Это будет контекстный менеджер, который считает время, проведенное внутри него. То есть у нас при открытии блока что-то произошло, и при закрытии мы должны вывести время, которое произошло внутри контекстного менеджера. Давайте напишем наш класс, назовем его `timer` и определим сразу методы `__enter__` и `__exit__`, потому что именно они говорят нам о том, что это контекстный менеджер. Отлично." ] }, { "cell_type": "code", "execution_count": 9, "id": "bdca5011", "metadata": {}, "outputs": [], "source": [ "import time\n", "\n", "class timer():\n", " def __init__(self):\n", " self.start = time.time()\n", " \n", " def current_time(self):\n", " return time.time() - self.start\n", " \n", " def __enter__(self):\n", " return self\n", " \n", " def __exit__(self, *args):\n", " print(f\"Elapsed: {self.current_time()}\")" ] }, { "cell_type": "markdown", "id": "6abe882b", "metadata": {}, "source": [ "Как мы будем использовать наш класс? Мы будем использовать оператор `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`. Давайте отформатируем строчку, чтобы было понятно, что именно выводится у нас. Итак. Запускаем наш контекстный менеджер." ] }, { "cell_type": "code", "execution_count": 10, "id": "6dd41c72", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Current: 1.0140066146850586\n", "Elapsed: 2.028013229370117\n" ] } ], "source": [ "with timer() as t:\n", " time.sleep(1)\n", " print(f\"Current: {t.current_time()}\")\n", " time.sleep(1)" ] }, { "cell_type": "markdown", "id": "7c052354", "metadata": {}, "source": [ "Итак, у нас вначале выводится время текущее, то есть прошла одна секунда, один `sleep`, и итоговое время две секунды с небольшим. Собственно, как мы и ожидали. Мы написали с вами контекстный менеджер, который считает время, проведенное внутри него и плюс еще позволяет вам вывести время, которое прошло с момента начала внутри самого контекстного менеджера. Итак, контекстные менеджеры позволяют вам определить поведение при входе и выходе из блока кода `with`, что часто бывает полезно, например, при закрытии каких-то ресурсов или, например, при транзакционной какой-то деятельности." ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.6.8" } }, "nbformat": 4, "nbformat_minor": 5 }