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.

801 lines
31 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": "f647bc43",
"metadata": {},
"source": [
"# Магические методы #"
]
},
{
"cell_type": "markdown",
"id": "fc2cfee7",
"metadata": {},
"source": [
"Настало время поговорить о дескрипторах. С помощью дескрипторов в Python реализована практически вся магия при работе с объектами, классами и методами.\n",
"\n",
"Чтобы определить свой собственный дескриптор, нужно определить класс и задать методы `__get__`, `__set__` или `__delete__`."
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "69fc5558",
"metadata": {},
"outputs": [],
"source": [
"class Descriptor:\n",
" def __get__(self, obj, obj_type):\n",
" print(\"get\")\n",
" \n",
" def __set__(self, obj, value):\n",
" print(\"set\")\n",
" \n",
" def __delete__(self, obj):\n",
" print(\"delete\")"
]
},
{
"cell_type": "markdown",
"id": "6f32656e",
"metadata": {},
"source": [
"После этого мы можем создать какой-то новый класс и в атрибут этого класса записать объект типа дескриптор."
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "22ef5e35",
"metadata": {},
"outputs": [],
"source": [
"class Class:\n",
" attr = Descriptor()"
]
},
{
"cell_type": "markdown",
"id": "56d62171",
"metadata": {},
"source": [
"Таким образом, наш атрибут будет являться дескриптором. Что это значит? У него будет переопределено поведение при доступе к атрибуту, при присваивании значений или при удалении.\n",
"\n",
"Метод `__get__`, как вы могли догадаться, определяет поведение при доступе к атрибуту. Метод `__set__` будет переопределять какое-то поведение, если мы попытаемся в наш атрибут что-то присвоить, а метод `__delete__` будет говорить о том, что будет происходить, если мы удалим наш атрибут.\n",
"\n",
"Мы создадим объект класса `Class` и посмотрим, что будет происходить при обращении к атрибуту."
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "6350d18c",
"metadata": {},
"outputs": [],
"source": [
"instance = Class()"
]
},
{
"cell_type": "markdown",
"id": "e678e3f9",
"metadata": {},
"source": [
"Если мы просто попытаемся вывести наш атрибут, у нас вызовется метод `__get__`."
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "1c4f3b2c",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"get\n"
]
}
],
"source": [
"instance.attr"
]
},
{
"cell_type": "markdown",
"id": "e3ff5546",
"metadata": {},
"source": [
"Если мы запишем в него какое-то значение, у нас вызывается метод `__set__`."
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "c8ff0399",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"set\n"
]
}
],
"source": [
"instance.attr = 10"
]
},
{
"cell_type": "markdown",
"id": "ae92ca69",
"metadata": {},
"source": [
"А если мы его удаляем, вызывается метод `__delete__`."
]
},
{
"cell_type": "code",
"execution_count": 7,
"id": "38c4d579",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"delete\n"
]
}
],
"source": [
"del instance.attr"
]
},
{
"cell_type": "markdown",
"id": "be2f46c0",
"metadata": {},
"source": [
"Таким образом, Python позволяет вам переопределять поведение при доступе к атрибуту. Это очень мощная концепция, мощный механизм, который позволяет вам незаметно от пользователя определять различные поведения в ваших классах.\n",
"\n",
"Например, мы можем определить дескриптор `Value`, который будет переопределять поведение при присваивании значения в него."
]
},
{
"cell_type": "code",
"execution_count": 8,
"id": "bfcc07af",
"metadata": {},
"outputs": [],
"source": [
"class Value:\n",
" def __init__(self):\n",
" self.value = None\n",
" \n",
" @staticmethod\n",
" def _prepare_value(value):\n",
" return value * 10\n",
" \n",
" def __get__(self, obj, obj_type):\n",
" return self.value\n",
" \n",
" def __set__(self, obj, value):\n",
" self.value = self._prepare_value(value)"
]
},
{
"cell_type": "markdown",
"id": "0f54defa",
"metadata": {},
"source": [
"Мы определим наш класс с атрибутом, который будет являться дескриптором, и при присваивании значений в дескриптор у нас будет происходить модифицированное поведение."
]
},
{
"cell_type": "code",
"execution_count": 10,
"id": "c6e4992a",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"100\n"
]
}
],
"source": [
"class Class:\n",
" attr = Value()\n",
" \n",
"instance = Class()\n",
"instance.attr = 10\n",
"\n",
"print(instance.attr)"
]
},
{
"cell_type": "markdown",
"id": "cc9892f2",
"metadata": {},
"source": [
"То есть наш метод `__set__` говорит о том, что, когда мы присваиваем значение в наш дескриптор, мы не просто сохраняем это значение, но мы как-то его препроцессим. В данном случае просто умножаем на десять. Таким образом, когда мы присваиваем десятку в наш атрибут, который является дескриптором, у нас, на самом деле, сохраняется сотня. В данном случае мы переопределили только два метода `__get__` и `__set__` этого уже достаточно для того, чтобы наш класс являлся дескриптором. Вы можете переопределить любой из трех методов, и класс уже будет являться дескриптором.\n",
"\n",
"Если у вас переопределен только метод `__get__`, то это `non-data` дескриптор, если `__set__` или `__delete__` — то это `data` дескриптор. Это говорит о том, в каком порядке они будут искаться, вызываться при поиске атрибутов."
]
},
{
"cell_type": "markdown",
"id": "fdbaee85",
"metadata": {},
"source": [
"### Напишем дескриптор, который пишет в файл все присваиваемые ему значения ###"
]
},
{
"cell_type": "markdown",
"id": "2dd70eb1",
"metadata": {},
"source": [
"Давайте, чтобы подробнее разобраться с тем, как работают дескрипторы, напишем, как всегда, свой дескриптор. И в данном случае это будет дескриптор, который будет записывать все значения, которые ему присваиваются, в файл.\n",
"\n",
"Мы можем представить, что это какая-то важная информация, которую всегда нужно сохранять. Можете это сохранять в данном случае в файл или, например, сохранять куда-нибудь на сервер, делать реплику.\n",
"\n",
"Давайте создадим наш класс `ImportantValue` и переопределим его методы `__get__`. Метод `__get__` принимает `obj` и `obj_type`, то есть объект, с которым вызвал дескриптор его тип, и метод `__set__`, который принимает объект, и значение, которое нужно присвоить. "
]
},
{
"cell_type": "code",
"execution_count": 11,
"id": "957da1db",
"metadata": {},
"outputs": [],
"source": [
"class ImportantValue:\n",
" def __get__(self, obj, obj_type):\n",
" pass\n",
" \n",
" def __set__(self, obj, value):\n",
" pass"
]
},
{
"cell_type": "markdown",
"id": "240e5f9a",
"metadata": {},
"source": [
"Таким образом, если мы создадим класс с какой-то важной информацией, например, это может быть класс `Account`, и важная информация — это, например, `amount` это какое-то, например, денежное значение, которое нам нужно всегда сохранять. И мы можем сделать это дескриптором."
]
},
{
"cell_type": "code",
"execution_count": 12,
"id": "35057bd3",
"metadata": {},
"outputs": [],
"source": [
"class Account:\n",
" amount = ImportantValue()"
]
},
{
"cell_type": "markdown",
"id": "6902c993",
"metadata": {},
"source": [
"В данном случае наш `amount` является дескриптором с переопределенным поведением. Однако пока ничего не происходит в этом переопределенном поведении. Давайте это исправим. Определим метод `__init__`, и наш дескриптор должен принимать какое-то значение, которые мы должны сохранить. В данном случае он принимает `amount` и, допустим, просто его сохраняет."
]
},
{
"cell_type": "code",
"execution_count": 13,
"id": "b842c9cc",
"metadata": {},
"outputs": [],
"source": [
"class ImportantValue:\n",
" def __init__(self, amount):\n",
" self.amount = amount\n",
" \n",
" def __get__(self, obj, obj_type):\n",
" pass\n",
" \n",
" def __set__(self, obj, value):\n",
" pass"
]
},
{
"cell_type": "markdown",
"id": "589335ed",
"metadata": {},
"source": [
"Пусть у нас будет 100 каких-то единиц на счету."
]
},
{
"cell_type": "code",
"execution_count": 14,
"id": "e7a70ad0",
"metadata": {},
"outputs": [],
"source": [
"class Account:\n",
" amount = ImportantValue(100)"
]
},
{
"cell_type": "markdown",
"id": "3128afb5",
"metadata": {},
"source": [
"Если мы будем менять значение нашего атрибута, мы хотим все это логировать. Будем всегда логировать в один и тот же файл, просто записывать. Не забудем сохранить все-таки само значение.\n",
"\n",
"Когда мы пытаемся к нему обратиться, нужно вернуть это значение из `amount`."
]
},
{
"cell_type": "code",
"execution_count": 16,
"id": "f1c0da60",
"metadata": {},
"outputs": [],
"source": [
"class ImportantValue:\n",
" def __init__(self, amount):\n",
" self.amount = amount\n",
" \n",
" def __get__(self, obj, obj_type):\n",
" return self.amount\n",
" \n",
" def __set__(self, obj, value):\n",
" with open(\"log.txt\", \"w\") as f:\n",
" f.write(str(value))\n",
" \n",
" self.amount = value"
]
},
{
"cell_type": "markdown",
"id": "505b86ae",
"metadata": {},
"source": [
"Мы создали наш дескриптор. Давайте посмотрим, что будет происходить, когда мы попытаемся присвоить значение в `amount`. Для этого нам нужно создать объект класса `Account`. Пусть это будет `bobs_account`. И у Боба на счету какое-то количество единиц."
]
},
{
"cell_type": "code",
"execution_count": 17,
"id": "da7c4d5a",
"metadata": {},
"outputs": [],
"source": [
"class Account:\n",
" amount = ImportantValue(100)\n",
" \n",
"bobs_account = Account()"
]
},
{
"cell_type": "markdown",
"id": "5947625a",
"metadata": {},
"source": [
"Таким образом, если мы попытаемся это количество единиц, количество каких-то денег изменить, то есть мы сделаем `amount` не 100, например, а 150, эти изменения должны залогироваться в файл. Давайте проверим, есть ли они там. Мы просто читаем наш файл и смотрим, что туда записалось."
]
},
{
"cell_type": "code",
"execution_count": 20,
"id": "78fb9277",
"metadata": {},
"outputs": [],
"source": [
"bobs_account.amount = 150"
]
},
{
"cell_type": "code",
"execution_count": 21,
"id": "0981db7e",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"150"
]
}
],
"source": [
"! cat log.txt"
]
},
{
"cell_type": "markdown",
"id": "698ed728",
"metadata": {},
"source": [
"Отлично! В нашем файле 150, потому что, когда мы присваивали значения в наш дескриптор, мы не только эти значения запоминали, но и записывали в файл. Таким образом, если мы запишем, например, 200, у нас в файле будет 200."
]
},
{
"cell_type": "code",
"execution_count": 23,
"id": "82f2fd22",
"metadata": {
"scrolled": true
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"200\n"
]
}
],
"source": [
"bobs_account.amount = 200\n",
"\n",
"with open(\"log.txt\", \"r\") as f:\n",
" print(f.read())"
]
},
{
"cell_type": "markdown",
"id": "18fbc691",
"metadata": {},
"source": [
"Мы можем не перезаписывать этот файл, а, например, дополнять его. Таким образом, у нас каждый раз, когда мы будем присваивать, файл будет изменяться. Мы можем логировать все записи в этот атрибут."
]
},
{
"cell_type": "code",
"execution_count": 24,
"id": "074c0f7b",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"200150200250\n"
]
}
],
"source": [
"class ImportantValue:\n",
" def __init__(self, amount):\n",
" self.amount = amount\n",
" \n",
" def __get__(self, obj, obj_type):\n",
" return self.amount\n",
" \n",
" def __set__(self, obj, value):\n",
" with open(\"log.txt\", \"a\") as f:\n",
" f.write(str(value))\n",
" \n",
" self.amount = value\n",
" \n",
"class Account:\n",
" amount = ImportantValue(100)\n",
" \n",
"bobs_account = Account()\n",
"bobs_account.amount = 150\n",
"bobs_account.amount = 200\n",
"bobs_account.amount = 250\n",
"\n",
"with open(\"log.txt\", \"r\") as f:\n",
" print(f.read())"
]
},
{
"cell_type": "markdown",
"id": "fda9028f",
"metadata": {},
"source": [
"## Функции и методы ##"
]
},
{
"cell_type": "markdown",
"id": "b920bcd4",
"metadata": {},
"source": [
"Что ж, давайте продолжим. И на самом деле, несмотря на то, что вы пользовались функциями и методами уже довольно давно, на самом деле функции и методы реализованы с помощью дескрипторов. Чтобы понять, что это действительно так, можно попробовать обратиться к одному и тому же методу с помощью объекта и с помощью класса. И окажется, что, когда мы обращаемся к методу через точку от объекта, у нас возвращается `bound method`. То есть это метод, привязанный уже к какому-то объекту, в данном случае `object`. А если мы обращаемся к методу от `Class`, то у нас это `unbound method`. Это просто функция."
]
},
{
"cell_type": "code",
"execution_count": 25,
"id": "e68a9a79",
"metadata": {
"scrolled": true
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"<bound method Class.method of <__main__.Class object at 0x7ff390566828>>\n",
"<function Class.method at 0x7ff39056e0d0>\n"
]
}
],
"source": [
"class Class:\n",
" def method(self):\n",
" pass\n",
"\n",
"obj = Class()\n",
"\n",
"print(obj.method)\n",
"print(Class.method)"
]
},
{
"cell_type": "markdown",
"id": "c3862d17",
"metadata": {},
"source": [
"Как вы видите, один и тот же метод возвращает разные объекты в зависимости от того, как к нему обращаются. Это и есть поведение дескриптора.\n",
"\n",
"Вам уже знаком декоратор `property`, который позволяет вам использовать функцию как атрибут класса. В данном случае мы можем определить `property full_name`, который на самом деле хоть и является функцией, которая возвращает строчку, используется потом так же, как и обычный атрибут, то есть без вызова скобочек. В данном случае у нас класс `User`, у нас `first_name` и `last_name`, и `full_name` возвращает, очевидно, полное имя. При вызове `full_name` от объекта у нас вызывается функция `full_name`. Однако если мы пытаемся обратиться к `full_name` от класса, у нас получится объект типа `property`. На самом деле, `property` реализовано с помощью дескрипторов, потому что разное поведение в зависимости от того, как у нас вызывается этот объект."
]
},
{
"cell_type": "code",
"execution_count": 26,
"id": "28f4c5db",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Amy Jones\n",
"<property object at 0x7ff390568908>\n"
]
}
],
"source": [
"class User:\n",
" def __init__(self, first_name, last_name):\n",
" self.first_name = first_name\n",
" self.last_name = last_name\n",
" \n",
" @property\n",
" def full_name(self):\n",
" return f\"{self.first_name} {self.last_name}\"\n",
"\n",
"amy = User(\"Amy\", \"Jones\")\n",
"\n",
"print(amy.full_name)\n",
"print(User.full_name)"
]
},
{
"cell_type": "markdown",
"id": "72f7b367",
"metadata": {},
"source": [
"И мы можем написать свой собственный класс `property`, который будет эмулировать поведение стандартного `property`. Для этого нам нужно сохранить функцию, которую `property` получает, потому что `property` — это декоратор, он получает функцию. И когда мы обращаемся к нашему объекту, если он вызван от класса, мы просто возвращаем самого себя, а если у нас вызван наш атрибут с объектом, то мы возвращаем соответствующий `getter`, вызываем функцию."
]
},
{
"cell_type": "code",
"execution_count": 27,
"id": "3083b097",
"metadata": {},
"outputs": [],
"source": [
"class Property:\n",
" def __init__(self, getter):\n",
" self.getter = getter\n",
" \n",
" def __get__(self, obj, obj_type=None):\n",
" if obj is None:\n",
" return self\n",
" \n",
" return self.getter(obj)"
]
},
{
"cell_type": "markdown",
"id": "5c7975a4",
"metadata": {},
"source": [
"Таким образом, мы можем определить класс и использовать как стандартный декоратор `property`, так и новый только что созданный. В двух видах можем просто его использовать как декоратор с помощью синтаксического сахара, можем использовать как вызов функции."
]
},
{
"cell_type": "code",
"execution_count": 29,
"id": "39ec49f2",
"metadata": {},
"outputs": [],
"source": [
"class Class:\n",
" @property\n",
" def original(self):\n",
" return 'original'\n",
" \n",
" @Property\n",
" def custom_sugar(self):\n",
" return 'custom sugar'\n",
" \n",
" def custom_pure(self):\n",
" return 'custom pure'\n",
"\n",
" custom_pure = Property(custom_pure)"
]
},
{
"cell_type": "markdown",
"id": "8508693d",
"metadata": {},
"source": [
"И окажется, что они работают идентично, потому что на самом деле `property` реализован именно с помощью дескриптора."
]
},
{
"cell_type": "code",
"execution_count": 30,
"id": "4c4aa005",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"original\n",
"custom sugar\n",
"custom pure\n"
]
}
],
"source": [
"obj = Class()\n",
"\n",
"print(obj.original)\n",
"print(obj.custom_sugar)\n",
"print(obj.custom_pure)"
]
},
{
"cell_type": "markdown",
"id": "e112eb48",
"metadata": {},
"source": [
"Точно так же реализованы `StaticMethod` и `ClassMethod`. И мы можем точно так же написать свою реализацию `ClassMethod` и `StaticMethod`. `StaticMethod` просто сохраняет функцию. И когда она вызывается, когда мы пытаемся получить соответствующий атрибут, мы просто ее возвращаем, потому что это статический метод, нам не нужно передавать туда ни `self`, ни `class`."
]
},
{
"cell_type": "code",
"execution_count": 31,
"id": "6e662007",
"metadata": {},
"outputs": [],
"source": [
"class StaticMethod:\n",
" def __init__(self, func):\n",
" self.func = func\n",
" \n",
" def __get__(self, obj, obj_type=None):\n",
" return self.func"
]
},
{
"cell_type": "markdown",
"id": "7984e41f",
"metadata": {},
"source": [
"А `ClassMethod`, когда мы вызываем нашу функцию от объекта, то есть наш `obj_type` равен нулю, равен `None`, то мы передаем соответствующий `obj_type` первым значением. Как видите, как и ожидается от `ClassMethod`. `ClassMethod` принимает первым значением `class`. Именно это и делает наша реализация `ClassMethod`."
]
},
{
"cell_type": "code",
"execution_count": 32,
"id": "1b6d514c",
"metadata": {},
"outputs": [],
"source": [
"class ClassMethod:\n",
" def __init__(self, func):\n",
" self.func = func\n",
" \n",
" def __get__(self, obj, obj_type=None):\n",
" if obj_type is None:\n",
" obj_type = type(obj)\n",
" \n",
" def new_func(*args, **kwargs):\n",
" return self.func(obj_type, *args, **kwargs)\n",
" \n",
" return new_func"
]
},
{
"cell_type": "markdown",
"id": "fbd29ba8",
"metadata": {},
"source": [
"## `__slots__` ##"
]
},
{
"cell_type": "markdown",
"id": "e948fdc7",
"metadata": {},
"source": [
"На самом деле с помощью дескрипторов в Python реализовано очень много чего, и, например, есть такая конструкция `__slots__`, которая работает тоже с помощью дескрипторов. `__slots__` позволяет вам определить класс, у которого есть жестко заданный набор атрибутов. Как вы знаете, когда мы создаем класс, у класса создается соответствующий словарь, в который мы записываем атрибуты, которые добавляются в объект. Очень часто это бывает излишне. У вас может быть огромное количество, например, объектов, и вы не хотите создавать каждый раз для каждого объекта словарь. Для этого приходит на помощь конструкция `__slots__`, которая вам позволяет жестко задать количество элементов, которые ваш класс может содержать.\n",
"\n",
"В данном случае мы говорим, что у нас в нашем классе должен быть только атрибут `anakin`. Собственно, он при инициализации и создается. Если мы попытаемся добавить в наш класс, в наш объект какой-то еще один атрибут, у нас ничего не получится, потому что у нас нет, собственно, справочника, нет `dict`, в который мы это записываем. И `__slots__` реализуется с помощью определения дескрипторов для каждого из атрибутов."
]
},
{
"cell_type": "code",
"execution_count": 33,
"id": "5b70d468",
"metadata": {},
"outputs": [
{
"ename": "AttributeError",
"evalue": "'Class' object has no attribute 'luke'",
"output_type": "error",
"traceback": [
"\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
"\u001b[0;31mAttributeError\u001b[0m Traceback (most recent call last)",
"\u001b[0;32m<ipython-input-33-d380bc2d2572>\u001b[0m in \u001b[0;36m<module>\u001b[0;34m\u001b[0m\n\u001b[1;32m 6\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 7\u001b[0m \u001b[0mobj\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mClass\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 8\u001b[0;31m \u001b[0mobj\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mluke\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m\"the chosen too\"\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m",
"\u001b[0;31mAttributeError\u001b[0m: 'Class' object has no attribute 'luke'"
]
}
],
"source": [
"class Class:\n",
" __slots__ = [\"anakin\"]\n",
" \n",
" def __init__(self):\n",
" self.anakin = \"the chosen one\"\n",
"\n",
"obj = Class()\n",
"obj.luke = \"the chosen too\""
]
},
{
"cell_type": "markdown",
"id": "b48293d5",
"metadata": {},
"source": [
"Мы познакомились с вами с дескрипторами, узнали, как они на самом деле работают, и на самом деле с помощью дескрипторов реализована практически вся магия, вся работа с методами и функциями в Python."
]
}
],
"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
}