{
 "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
}
