{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "d6cf097e",
   "metadata": {},
   "source": [
    "# Магические методы #"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6db9e5ec",
   "metadata": {},
   "source": [
    "Привет. Добро пожаловать на четвёртый блок нашего с вами курса. В прошлом блоке вы разбирали объектно-ориентированное программирование на Python'е. А в этом блоке мы поподробнее познакомимся о том, как на самом деле всё работает внутри - как создаются объекты, как создаются классы, что происходит при выполнении определённых методов, и так далее.\n",
    "\n",
    "Давайте начнём с магических методов, с частью из которых вы уже знакомы. Итак, магический метод — это метод, определённый внутри класса, который начинается и заканчивается с двух подчёркиваний. Например, магическим методом является метод `__init__`, который отвечает за инициализацию созданного объекта. Давайте определим класс `User`, который будет переопределять магический метод `__init__`. Мы просто будем записывать полученые имя и email в атрибуты класса. Ну и можно определить, например, метод, который возвращает словарь."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "id": "f8cc84e5",
   "metadata": {},
   "outputs": [],
   "source": [
    "class User:\n",
    "    def __init__(self, name, email):\n",
    "        self.name = name\n",
    "        self.email = email\n",
    "        \n",
    "    def get_email_data(self):\n",
    "        return {\n",
    "            \"name\": self.name,\n",
    "            \"email\": self.email\n",
    "        }"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bb1f0133",
   "metadata": {},
   "source": [
    "Теперь при создании класса мы передадим атрибуты, которые запишутся в соответствующие значения нашего объекта. Ну и мы можем вызвать какой-то метод. С этим вы уже должны быть знакомы, потому что работали с классами уже довольно долго."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "id": "156d080e",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "{'name': 'Jane Doe', 'email': 'janedoe@example.com'}\n"
     ]
    }
   ],
   "source": [
    "jane = User(\"Jane Doe\", \"janedoe@example.com\")\n",
    "print(jane.get_email_data())"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "05b3d341",
   "metadata": {},
   "source": [
    "Ещё одним магическим методом является метод `__new__`, который отвечает за то, что происходит в момент создания объекта класса. Метод `__new__` возвращает только что созданный объект класса и может как-то переопределять поведение при создании. Например, можно создать класс `Singleton`, который гарантирует то, что объект может быть создан только один от этого класса."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "id": "81dbbd40",
   "metadata": {},
   "outputs": [],
   "source": [
    "class Singleton:\n",
    "    instance = None\n",
    "    \n",
    "    def __new__(cls):\n",
    "        if cls.instance is None:\n",
    "            cls.instance = super().__new__(cls)\n",
    "            \n",
    "        return cls.instance"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "700b1e8e",
   "metadata": {},
   "source": [
    "Например, мы можем попытаться создать два объекта `a` и `b`, и окажется, что это один и тот же объект, потому что мы проверяем, был ли уже создан какой-то объект, и, собственно, если он уже создан, мы не будем создавать новый объект."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "id": "15a2b9d0",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "True"
      ]
     },
     "execution_count": 17,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "a = Singleton()\n",
    "b = Singleton()\n",
    "\n",
    "a is b"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0714f906",
   "metadata": {},
   "source": [
    "Существует также метод `__del__`, который определяет поведение при удалении объекта. Однако, он работает не всегда очевидно. Он вызывается не когда мы удаляем объект оператором `del`, а когда количество ссылок на наш объект стало равно нулю и вызывается `garbage collector`. Это не всегда происходит тогда, когда мы думаем это должно произойти, поэтому переопределять его нежелательно. Будьте внимательны.\n",
    "\n",
    "Собственно, на этой лекции мы просмотрим какой-то набор магических методов, который чаще всего используется и переопределяется. Посмотрим, как они себя ведут и как писать классы, которые их используют.\n",
    "\n",
    "Одним из магических методов является метод `__str__`, который определяет поведение, например, при вызове функции `print`. Метод `__str__` должен определить какое-то человекочитаемое описание нашего класса, которое может вывести потом пользователь где-то, например, в интерфейсе. Классическим вариантом метода `__str__` может быть выведение имени и email."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "id": "224f2c77",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Jane Doe <janedoe@example.com>\n"
     ]
    }
   ],
   "source": [
    "class User:\n",
    "    def __init__(self, name, email):\n",
    "        self.name = name\n",
    "        self.email = email\n",
    "        \n",
    "    def __str__(self):\n",
    "        return f\"{self.name} <{self.email}>\"\n",
    "    \n",
    "jane = User(\"Jane Doe\", \"janedoe@example.com\")\n",
    "print(jane)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "506ab28e",
   "metadata": {},
   "source": [
    "Обратите внимание, мы используем тот же самый класс, что и раньше, но теперь, если мы будем принтить наш объект, у нас будет не какой-то `object` типа `user`, а понятное и читаемое название нашего объекта.\n",
    "\n",
    "Ещё двумя полезными методами магическими являются методы `__hash__` и `equal`,`__eq__`, которые определяют то, как сравниваются наши объекты и что происходит при вызове функции `hash`. Магический метод `__hash__` может, например, переопределить функцию хеширования, которая используется, например, когда мы получаем ключи в словаре."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "id": "aff3ed05",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "True\n"
     ]
    }
   ],
   "source": [
    "class User:\n",
    "    def __init__(self, name, email):\n",
    "        self.name = name\n",
    "        self.email = email\n",
    "        \n",
    "    def __hash__(self):\n",
    "        return hash(self.email)\n",
    "    \n",
    "    def __eq__(self, obj):\n",
    "        return self.email == obj.email\n",
    "    \n",
    "jane = User(\"Jane Doe\", \"jdoe@example.com\")\n",
    "joe = User(\"Joe Doe\", \"jdoe@example.com\")\n",
    "\n",
    "print(jane == joe)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bed51c07",
   "metadata": {},
   "source": [
    "В данном случае нашего класса `user` мы можем сказать, что при вызове функции `__hash__` мы хотим хешировать email, то есть у нас в качестве хеша берётся всегда имейл пользователя. Ну а при сравнении мы эти email просто сравниваем, при сравнении двух объектов. Таким образом, если мы создадим двух юзеров с разными именами, но одинаковыми имейлами, при вызове функции сравнения, то есть когда мы используем `==`, Python будет говорить нам, что это один и тот же объект, потому что вызывается метод `__eq__`, который сравнивает только email. Точно так же функция `__hash__` возвращает теперь одно и то же значение, потому что используется значение email, которое в данном случае одинаковое.\n",
    "\n",
    "Но если мы попробуем создать словарь, где в качестве ключа будет использоваться наш объект юзера, то у нас создастся словарь только с одним ключом, а не с двумя объектами, несмотря на то, что мы итерируемся здесь по двум объектам, потому что в качестве хеша используется одно и то же значение, и в качестве `__eq__` у нас сравниваются email."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "8c53d626",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "2122083289\n",
      "2122083289\n"
     ]
    }
   ],
   "source": [
    "print(hash(jane))\n",
    "print(hash(joe))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "6ebd5adf",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "{<__main__.User object at 0x038176E8>: 'Joe Doe'}\n"
     ]
    }
   ],
   "source": [
    "user_email_map = {user: user.name for user in [jane, joe]}\n",
    "\n",
    "print(user_email_map)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5301ba65",
   "metadata": {},
   "source": [
    "Очень важными магическими методами являются методы, определяющие доступ к атрибутам. Это методы `__getattr__` и `__getattribute__`. Очень важно понимать между ними отличия, потому что очень часто происходит путаница. Итак, метод `__getattr__` определяет поведение, когда наш атрибут, который мы пытаемся получить, не найден. Таким образом, если мы обратимся к атрибуту какого-то объекта и у нас он будет не найден, у нас всегда вызовется метод `__getattr__` и мы можем определить какое-то поведение дефолтное при той ситуации, когда атрибута нет.\n",
    "\n",
    "Метод `__getattribute__` вызывается в любом случае. Когда мы обращаемся к какому-то атрибуту, у нас всегда вызывается метод `__getattribute__`, и мы можем, например, логировать какие-то обращения к атрибутам или переопределять поведение при поиске соответствующих атрибутов объекта. Например, мы можем возвращать всегда какую-то строчку и ничего не делать, как в данном случае."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "fdfe0e1e",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "nope\n",
      "nope\n",
      "nope\n"
     ]
    }
   ],
   "source": [
    "class Researcher:\n",
    "    def __getattr__(self, name):\n",
    "        return \"Nothing found :(\"\n",
    "    \n",
    "    def __getattribute__(self, name):\n",
    "        return \"nope\"\n",
    "    \n",
    "obj = Researcher()\n",
    "\n",
    "print(obj.attr)\n",
    "print(obj.method)\n",
    "print(obj.DFG2H3J00KLL)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d70a8034",
   "metadata": {},
   "source": [
    "Мы определили класс и переопределили метод `__getattribute__`, который всегда возвращает строку и ничего дальше не делает. Таким образом, что бы мы не делали, как бы мы не пытались обращаться к атрибутам, даже которых ещё нет, как в данном случае, у нас всегда выведется эта строка. `__getattr__` работает немного по-другому. `__getattr__`, как я уже говорил, вызывается в том случае, если атрибут не найден."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "2b992fcb",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Looking for attr\n",
      "Nothing found :(\n",
      "Looking for method\n",
      "Nothing found :(\n",
      "Looking for DFG2H3J00KLL\n",
      "Nothing found :(\n"
     ]
    }
   ],
   "source": [
    "class Researcher:\n",
    "    def __getattr__(self, name):\n",
    "        return \"Nothing found :(\"\n",
    "    \n",
    "    def __getattribute__(self, name):\n",
    "        print(f\"Looking for {name}\")\n",
    "        return object.__getattribute__(self, name)\n",
    "    \n",
    "obj = Researcher()\n",
    "\n",
    "print(obj.attr)\n",
    "print(obj.method)\n",
    "print(obj.DFG2H3J00KLL)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "981d22e6",
   "metadata": {},
   "source": [
    "В данном случае внутри `__getattribute__`, который вызывается всегда, мы просто логируем, что мы пытаемся найти соответствующий атрибут и продолжаем выполнение, используя класс `object`. Таким образом, если у нас пытается найтись атрибут, которого не существует, у нас вызовется `__getattr__`, что здесь и происходит. У нас ничего не найдено. Отлично."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "aa031adb",
   "metadata": {},
   "source": [
    "`__setattr__`, как вы могли догадаться, определяет поведение при присваивании значения к атрибуту. Например, вместо того, чтобы присвоить значение, мы можем опять же вернуть какую-то строчку и ничего не делать. В данном случае, если мы попытаемся присвоить значение атрибуту, у нас ничего не произойдёт и атрибут не создастся."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "id": "7b83ad70",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Not gonna set math!\n"
     ]
    }
   ],
   "source": [
    "class Ignorant:\n",
    "    def __setattr__(self, name, value):\n",
    "        print(f\"Not gonna set {name}!\")\n",
    "\n",
    "obj = Ignorant()\n",
    "obj.math = True"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "id": "fb3f8c31",
   "metadata": {},
   "outputs": [
    {
     "ename": "AttributeError",
     "evalue": "'Ignorant' object has no attribute 'math'",
     "output_type": "error",
     "traceback": [
      "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m",
      "\u001b[1;31mAttributeError\u001b[0m                            Traceback (most recent call last)",
      "\u001b[1;32m<ipython-input-20-f41a5f04e4f0>\u001b[0m in \u001b[0;36m<module>\u001b[1;34m\u001b[0m\n\u001b[1;32m----> 1\u001b[1;33m \u001b[0mprint\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mobj\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mmath\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m",
      "\u001b[1;31mAttributeError\u001b[0m: 'Ignorant' object has no attribute 'math'"
     ]
    }
   ],
   "source": [
    "print(obj.math)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "852a889f",
   "metadata": {},
   "source": [
    "Ну а `__delattr__` управляет поведением, когда мы пытаемся удалить какой-то атрибут объекта. Мы можем не удалять, а, например, переопределить как-то поведение или добавить какую-то функциональность. Например, если мы хотим каскадно удалить какие-то объекты, связанные с нашим классом."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "id": "684b1176",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Goodbye attr, you were 10!\n"
     ]
    }
   ],
   "source": [
    "class Polite:\n",
    "    def __delattr__(self, name):\n",
    "        value = getattr(self, name)\n",
    "        print(f\"Goodbye {name}, you were {value}!\")\n",
    "        object.__delattr__(self, name)\n",
    "\n",
    "obj = Polite()\n",
    "obj.attr = 10\n",
    "del obj.attr"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "dffcc1d4",
   "metadata": {},
   "source": [
    "В данном случае мы просто продолжаем удаление с помощью класса `object`, ну и как-то логируем то, что у нас происходит удаление.\n",
    "\n",
    "Ещё одним методом является метод `__call__`, который в соответствии со своим названием определяет поведение, когда наш класс вызывается. Например, с помощью метода `__call__` мы можем определить logger, который будем потом использовать в качестве декоратора. Да, декоратором может быть не только функция, но и класс."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "id": "f16c0418",
   "metadata": {},
   "outputs": [],
   "source": [
    "class Logger:\n",
    "    def __init__(self, filename):\n",
    "        self.filename = filename\n",
    "    def __call__(self, func):\n",
    "        with open(self.filename, \"w\") as f:\n",
    "            f.write(\"Oh Danny boy...\")\n",
    "            \n",
    "        return func\n",
    "\n",
    "logger = Logger(\"log.txt\")\n",
    "\n",
    "@logger\n",
    "def completely_useless_function():\n",
    "    pass"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 24,
   "id": "2df4685b",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Oh Danny boy...\n"
     ]
    }
   ],
   "source": [
    "completely_useless_function()\n",
    "\n",
    "with open('log.txt') as f:\n",
    "    print(f.read())"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 25,
   "id": "0decbc80",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Oh Danny boy...\n"
     ]
    }
   ],
   "source": [
    "! type log.txt"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c51d3828",
   "metadata": {},
   "source": [
    "В данном случае при инициализации класса мы запоминаем `filename`, который ему передан, и каждый раз, когда мы будем вызывать наш класс, мы будем возвращать какую-то новую функцию в соответствии с протоколом декораторов и записывать значения, записывать какую-то строчку о том, что у нас наша функция вызвана. В данном случае, мы просто определяем пустую функцию, декоратор которой записывает все её вызовы. Ну и когда мы вызовем нашу функцию, у нас в нашем файле появится строка."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7c12bb61",
   "metadata": {},
   "source": [
    "Классическим примером на перегрузку операторов в других языках является перегрузка оператора сложения. В данном случае за операцию сложения в Python'е отвечает оператор `__add__`. Существуют также другие операторы вроде `__sub__`, который определяет поведение при вычитании, что логично. И мы можем определить наш класс, который будет перегружать операцию сложения."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 26,
   "id": "6e7dab94",
   "metadata": {},
   "outputs": [],
   "source": [
    "import random\n",
    "\n",
    "class NoisyInt:\n",
    "    def __init__(self, value):\n",
    "        self.value = value\n",
    "    def __add__(self, obj):\n",
    "        noise = random.uniform(-1, 1)\n",
    "        return self.value + obj.value + noise\n",
    "\n",
    "a = NoisyInt(10)\n",
    "b = NoisyInt(20)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7b189df8",
   "metadata": {},
   "source": [
    "В данном случае мы можем написать какой-то `NoisyInt`, который будет работать почти как `integer`, но добавлять какой-то шум, например, при сложении. Мы определим два наших Int'а со значением 10 и 20, и в операции сложение, то есть в методе `__add__` мы будем добавлять какое-то случайное значение от минус единицы до единицы. Таким образом, когда мы попытаемся сложить два наших числа, у нас выведется число в окрестности искомого, то есть у нас будет 29.5, например, вместо 30."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 27,
   "id": "5eb86601",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "29.838970125536626\n",
      "29.283700512158727\n",
      "30.074755836157177\n"
     ]
    }
   ],
   "source": [
    "for _ in range(3):\n",
    "    print(a + b)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "84513ba8",
   "metadata": {},
   "source": [
    "Это просто пример, вы можете переопределять операцию сложения так, как вам удобно, как вам подходит для вашей задачи. \n",
    "\n",
    "Предлагаю вам попробовать попрактиковаться и самостоятельно написать контейнер с помощью методов `__getitem__` и `__setitem__`."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9bed86fa",
   "metadata": {},
   "source": [
    "Отлично, надеюсь, у вас получилось. Ну а в качестве примера я реализовал свой собственный класс `PascalList`, который имитирует поведение списков в Паскале. Как вы знаете, в Python'e списки нумеруются с нуля, ну а в Паскале — с единицы. Мы можем переопределить методы `__getitem__` и `__setitem__` так, чтобы они работали как в Паскале. Методы `__getitem__` и `__setitem__` определяют поведение, когда мы работаем с нашим классом с помощью квадратных скобочек, обращаясь по какому-то индексу, то есть как в случае с list'ами, со списками, или в случае со словарями."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 28,
   "id": "805d5032",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "1\n"
     ]
    }
   ],
   "source": [
    "class PascalList:\n",
    "    def __init__(self, original_list=None):\n",
    "        self.container = original_list or []\n",
    "        \n",
    "    def __getitem__(self, index):\n",
    "        return self.container[index - 1]\n",
    "    \n",
    "    def __setitem__(self, index, value):\n",
    "        self.container[index - 1] = value\n",
    "        \n",
    "    def __str__(self):\n",
    "        return self.container.__str__()\n",
    "    \n",
    "numbers = PascalList([1, 2, 3, 4, 5])\n",
    "print(numbers[1])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 29,
   "id": "ff758ccd",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[1, 2, 3, 4, 25]\n"
     ]
    }
   ],
   "source": [
    "numbers[5] = 25\n",
    "\n",
    "print(numbers)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1475fc66",
   "metadata": {},
   "source": [
    "И в данном примере мы определяем класс `PascalList`, который принимает какой-то список, запоминает его и каждый раз, когда мы пытаемся достучаться до какого-то индекса, он обращается к этому индексу минус единица. Таким образом, если мы попытаемся взять первый элемент, у нас, на самом деле, возьмётся нулевой элемент, как в Python'e. Ну и, например, мы можем создать `PascalList` от одного до пяти, и, обратившись по первому индексу, мы получим элемент один, как и сделали бы в Паскале. Ну и, например, к пятому мы можем переопределить значение в конце.\n",
    "\n",
    "Собственно, на этом всё, мы с вами обсудили какой-то набор магических методов. Более полно можно посмотреть про них в документации. Их огромное количество, и они управляют поведением класса в Python'e и тем, как работают объекты, созданные от этих классов."
   ]
  }
 ],
 "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.8.8"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
