You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

295 lines
18 KiB
Plaintext

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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