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