{ "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": [ ">\n", "\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", "\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\u001b[0m in \u001b[0;36m\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 }