{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "90fa98ae",
   "metadata": {},
   "source": [
    "# Метаклассы #"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a49613fc",
   "metadata": {},
   "source": [
    "В этой лекции мы поговорим с вами о мета-классах. Как вы уже знаете, всё в Python'е является объектом, и классы не исключение, а значит эти классы кто-то создаёт. Давайте определим класс с названием Class и его объект. Тип нашего объекта является Class, потому что Class создал наш объект."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "341462e8",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "__main__.Class"
      ]
     },
     "execution_count": 1,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "class Class:\n",
    "    ...\n",
    "    \n",
    "obj = Class()\n",
    "type(obj)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5c17b22a",
   "metadata": {},
   "source": [
    "Однако, у класса тоже есть тип. Этот тип `type`, потому что `type` создал наш класс."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "03dc5e19",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "type"
      ]
     },
     "execution_count": 2,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "type(Class)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9e88bd36",
   "metadata": {},
   "source": [
    "В данном случае `type` является мета-классом. Он создаёт другие классы. Типом самого `type`, кстати, является он же сам."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "7c5cab2a",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "type"
      ]
     },
     "execution_count": 3,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "type(type)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "27b06d18",
   "metadata": {},
   "source": [
    "Это рекурсивное замыкание, которое реализовано с помощью С внутри. Очень важно понимать разницу между созданием и наследованием."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "fd58b7cb",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "False"
      ]
     },
     "execution_count": 4,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "issubclass(Class, type)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "75d5b828",
   "metadata": {},
   "source": [
    "В данном случае класс не является `subclass`'ом `type`. `Type` его создаёт, но класс не наследуется от него. Класс наследуется от класса объекта."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "410a1c95",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "True"
      ]
     },
     "execution_count": 5,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "issubclass(Class, object)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "114f8dd7",
   "metadata": {},
   "source": [
    "Чтобы понять, как вообще классы задаются, можно написать простую функцию, которая класс возвращает."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "21dc42eb",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "False\n"
     ]
    }
   ],
   "source": [
    "def dummy_factory():\n",
    "    class Class:\n",
    "        pass\n",
    "    \n",
    "    return Class\n",
    "\n",
    "Dummy = dummy_factory()\n",
    "print(Dummy() is Dummy())"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d2107543",
   "metadata": {},
   "source": [
    "В данном случае мы определяем функцию, которая возвращает какой-то новый класс — `dummy_factory`. Классы можно создавать на лету. В данном случае мы создаём два разных объекта и просто их возвращаем.\n",
    "\n",
    "Однако, на самом деле, Python работает, конечно, не так. Для создания классов используется мета-класс `type`, и вы можете на лету создать `type`, просто вызвав его и передав название класса."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "8186063e",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "<class '__main__.NewClass'>\n",
      "<__main__.NewClass object at 0x7f993b956d68>\n"
     ]
    }
   ],
   "source": [
    "NewClass = type(\"NewClass\", (), {})\n",
    "\n",
    "print(NewClass)\n",
    "print(NewClass())"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "05936a27",
   "metadata": {},
   "source": [
    "В данном случае мы создаём класс `NewClass` без родителей и без каких-то атрибутов. `NewClass` действительно является классическим классом. Вы можете его вывести, можете создать какой-то объект этого класса. Это настоящий класс, мы создали его на лету без литерала `class`.\n",
    "\n",
    "Однако, чаще всего классы создаются всё-таки по-другому. Они создаются с помощью мета-классов, и в данном случае давайте определим свой собственный мета-класс, который будет управлять поведением при создании класса."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "c6a34b73",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Creating A\n"
     ]
    }
   ],
   "source": [
    "class Meta(type):\n",
    "    def __new__(cls, name, parents, attrs):\n",
    "        print(f\"Creating {name}\")\n",
    "        \n",
    "        if \"class_id\" not in attrs:\n",
    "            attrs[\"class_id\"] = name.lower()\n",
    "            \n",
    "        return super().__new__(cls, name, parents, attrs)\n",
    "\n",
    "class A(metaclass=Meta):\n",
    "    pass"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "596e5a1e",
   "metadata": {},
   "source": [
    "Мы определим класс `Meta`. Для того чтобы он бы мета-классом, он должен наследоваться от другого мета-класса. В данном случае это мета-класс `type`, базовый мета-класс. И, как вы уже знаете, метод `__new__` управляет поведением при создании объекта. В данном случае объектом является другой класс, поэтому мы можем изменять поведение при создании другого класса. Метод `__new__` принимает название класса, его родителей и какие-то атрибуты. Мы можем определить какой-то новый класс `A` и указать, что его мета-классом является наш мета-класс. Именно этот мета-класс и будет управлять поведением при создании нового класса. В данном случае мы выводим строчку о том, что у нас класс создаётся. При определении `class` у нас вызывается мета-класс и функция `__new__`, метод `__new__`. Мы выводим, что у нас наш класс создаётся. Записываем в какой-то атрибут нашего класса, в данном случае в атрибут `class_id`, значение."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "id": "f83fe579",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "A.class_id: 'a'\n"
     ]
    }
   ],
   "source": [
    "print(f\"A.class_id: '{A.class_id}'\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c17782d2",
   "metadata": {},
   "source": [
    "Таким образом, мы можем переопределить поведение при создании класса. Например, добавить ему какой-то атрибут или сделать что-нибудь другое."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "id": "1fa3f00a",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Initializing — Base\n",
      "Initializing — A\n",
      "Initializing — B\n"
     ]
    }
   ],
   "source": [
    "class Meta(type):\n",
    "    def __init__(cls, name, bases, attrs):\n",
    "        print(f\"Initializing — {name}\")\n",
    "        \n",
    "        if not hasattr(cls, \"registry\"):\n",
    "            cls.registry = {}\n",
    "        else:\n",
    "            cls.registry[name.lower()] = cls\n",
    "            \n",
    "        super().__init__(name, bases, attrs)\n",
    "\n",
    "class Base(metaclass=Meta): pass\n",
    "\n",
    "class A(Base): pass\n",
    "\n",
    "class B(Base): pass"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b4ba7f87",
   "metadata": {},
   "source": [
    "Например, мы можем определить мета-класс, который переопределяет функцию `__init__`, и в данном случае наш мета-класс будет логировать, запоминать все созданные подклассы. Давайте определим функцию `__init__`, которая будет вызываться при инициализации нашего объекта. В данном случае нашим объектом является класс. При инициализации класса у нас она будет вызываться. Наш `__init__` принимает те же самые аргументы, однако, делает немного другое. Он записывает свой собственный атрибут значения созданных классов. В данном случае, у нас вначале создаётся класс `Base`, мета-классом которого является `Meta`, и у него создаётся `registry`, в который мы потом будем записывать все его подклассы. Каждый раз, когда у нас создаётся какой-то класс, который наследуется от `Base`, мы записываем в наш `registry` соответствующее значение, то есть название созданного класса и ссылку на него, то есть объект `class`. И мы можем вывести теперь все подклассы нашего `Base`, просто обратившись к `registry`, ну или написав обращение к методу `subclasses`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "id": "d4c2e1a5",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "{'a': <class '__main__.A'>, 'b': <class '__main__.B'>}\n",
      "[<class '__main__.A'>, <class '__main__.B'>]\n"
     ]
    }
   ],
   "source": [
    "print(Base.registry)\n",
    "print(Base.__subclasses__())"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b9aae82f",
   "metadata": {},
   "source": [
    "## Абстрактные методы ##"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2245e2a3",
   "metadata": {},
   "source": [
    "Очень часто при работе с объектно-ориентированной парадигмой в Python'е возникают вопросы про абстрактные методы, потому что абстрактные методы являются центральным понятием, например, в языке программирования C++. В Python'е есть абстрактные методы, вы можете их использовать с помощью стандартной библиотеки `abc`.\n",
    "\n",
    "В данном случае здесь также работают мета-классы и мы можем определить абстрактный какой-то класс с методом `abstractmethod`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "id": "a7a09dc8",
   "metadata": {},
   "outputs": [],
   "source": [
    "from abc import ABCMeta, abstractmethod\n",
    "\n",
    "class Sender(metaclass=ABCMeta):\n",
    "    @abstractmethod\n",
    "    def send(self):\n",
    "        \"\"\"Do something\"\"\""
   ]
  },
  {
   "cell_type": "markdown",
   "id": "49c0d57b",
   "metadata": {},
   "source": [
    "О чём говорит наш декоратор `abstractmethod`? Что у нас не получится создать какой-то класс, не определив этот метод. То есть у нас метод абстрактный и мы обязаны его переопределить в классе, который наследуется от нашего класса. В данном случае у нас `Child` не переопределяет метод `send`, и поэтому вызывается ошибка."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "id": "05e82b55",
   "metadata": {},
   "outputs": [
    {
     "ename": "TypeError",
     "evalue": "Can't instantiate abstract class Child with abstract methods send",
     "output_type": "error",
     "traceback": [
      "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
      "\u001b[0;31mTypeError\u001b[0m                                 Traceback (most recent call last)",
      "\u001b[0;32m<ipython-input-15-a03e0b46bd6a>\u001b[0m in \u001b[0;36m<module>\u001b[0;34m\u001b[0m\n\u001b[1;32m      1\u001b[0m \u001b[0;32mclass\u001b[0m \u001b[0mChild\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mSender\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;32mpass\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m      2\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 3\u001b[0;31m \u001b[0mChild\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[0m",
      "\u001b[0;31mTypeError\u001b[0m: Can't instantiate abstract class Child with abstract methods send"
     ]
    }
   ],
   "source": [
    "class Child(Sender): pass\n",
    "\n",
    "Child()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3bdd199c",
   "metadata": {},
   "source": [
    "Если мы переопределим метод `send`, то всё будет, как мы хотели, у нас абстрактный метод переопределён, всё работает."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "id": "c0599a42",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<__main__.Child at 0x7f993b90fef0>"
      ]
     },
     "execution_count": 16,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "class Child(Sender):\n",
    "    def send(self):\n",
    "        print(\"Sending\")\n",
    "\n",
    "Child()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6ff82354",
   "metadata": {},
   "source": [
    "Ну и, на самом деле, абстрактные методы используются в Python'е довольно редко, чаще всего вызывается просто исключение `NotImplementedError`, которое говорит о том, что этот метод нужно реализовать. Программист, когда видит в определении класса, что вызывается в методе `raise NotImplementedError`, что этот класс нужно в потомке переопределить."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "id": "a7ef9b3f",
   "metadata": {},
   "outputs": [],
   "source": [
    "class PythonWay:\n",
    "    def send(self):\n",
    "        raise NotImplementedError"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "66529f93",
   "metadata": {},
   "source": [
    "Что ж, мы с вами разобрали, как работают мета-классы. На самом деле мета-класс — это просто класс, который создаёт другие классы, потому что классы тоже являются объектом. Мы посмотрели, как можно написать свой собственный мета-класс и переопределить поведение при создании другого класса. Также вы можете определять собственные абстрактные методы, если вам необходимо сказать о том, что в классе, который наследуется от вашего другого класса, обязательно должен реализован быть какой-то метод, какой-то интерфейс. На этом всё."
   ]
  }
 ],
 "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
}
