{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "20e9c323",
   "metadata": {},
   "source": [
    "# Наследование в Python #"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "19ca4a24",
   "metadata": {},
   "source": [
    "Вы уже знаете достаточно большое количество информации о языке Python.\n",
    "\n",
    "Например, то, что всё в Python — это объект, и это очень сильно отличает его от других языков программирования.\n",
    "\n",
    "Также вы умеете объявлять свои собственные классы, добавлять к ним атрибуты, создавать методы, а также создавать экземпляры этих классов и обращаться к атрибутам и методам.\n",
    "\n",
    "Однако это далеко не все возможности объектно-ориентированного языка Python. Сегодня мы углубимся в детали реализации классов и поговорим про наследование.\n",
    "\n",
    "Для чего же нужно наследование классов? Прежде всего оно нужно для изменения поведения конкретного класса, а также расширения его функционала. Давайте представим, что нам необходимо программировать процессы, которые происходят на нашей планете Земля, и мы хотим населить нашу планету Земля домашними питомцами.\n",
    "\n",
    "Предположим, у нас есть класс, назовем его \"питомец\" или \"домашний питомец\". И у каждого домашнего питомца есть имя."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "54da589d",
   "metadata": {},
   "outputs": [],
   "source": [
    "class Pet:\n",
    "    def __init__(self, name=None):\n",
    "        self.name = name"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3ed53134",
   "metadata": {},
   "source": [
    "Нам неинтересно населять планету Земля непонятными питомцами и давайте попробуем ее населить, например, собаками."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "103a7252",
   "metadata": {},
   "source": [
    "Для этого нам поможет наследование. Класс \"питомец\" может использоваться уже другими программистами, и нам не хотелось бы его менять. Поэтому давайте попробуем расширить его.\n",
    "\n",
    "Наследование в Python выглядит очень просто, мы объявляем класс \"собака\", класс `Dog`, и в скобках указываем родительский класс \"питомец\". Новый класс, созданный при помощи наследования, наследует все атрибуты и методы родительского класса. В данном случае класс \"питомец\" является родительским классом, его также еще называют базовым классом или суперклассом. А класс \"собака\" называется дочерним классом или классом-наследником."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "b28d0ecd",
   "metadata": {},
   "outputs": [],
   "source": [
    "class Dog(Pet):\n",
    "    def __init__(self, name, breed=None):\n",
    "        super().__init__(name)\n",
    "        self.breed = breed\n",
    "        \n",
    "    def say(self):\n",
    "        return f\"{self.name}: waw\""
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "7248d621",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Шарик\n",
      "Шарик: waw\n"
     ]
    }
   ],
   "source": [
    "dog = Dog(\"Шарик\", \"Доберман\")\n",
    "print(dog.name)\n",
    "print(dog.say())"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f61360a2",
   "metadata": {},
   "source": [
    "Давайте попробуем изменить поведение нашего класса \"питомец\". Для этого мы переопределим метод `init`, или инициализатор класса, и добавим новый атрибут `breed`, в котором будем хранить породу собаки. Если этот атрибут равен значению `None`, то порода не определена. Также обратите внимание, мы в нашем методе `init` вызываем метод `init` из родительского класса. Для этого мы пользуемся функцией `super`.\n",
    "\n",
    "Хочу обратить ваше внимание, что мы именно не скопировали код из родительского класса, а именно обратились, вызвали его, тем самым мы расширили поведение этого класса.\n",
    "\n",
    "Также мы можем добавить, кроме атрибутов, и собственный метод. Давайте назовем его `say` и напишем реализацию. Таким образом, наши питомцы, или наши собаки, умеют подавать голос, если мы обратимся к этому методу `say`.\n",
    "\n",
    "Ну что же, мы создали класс при помощи наследования, который решает нужные нам задачи. Вы можете создать свои классы, унаследовать от класса \"питомец\" и населить планету Земля различными животными, птицами, рыбами, котами, какими угодно."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "aecc21a3",
   "metadata": {},
   "source": [
    "В Python разрешено наследование от нескольких классов предков, или как это еще называется, множественное наследование. Очень часто этот прием используется для реализации класса примесей. \n",
    "\n",
    "Предположим, что нам необходимо экспортировать данные о наших объектах собачках в формате `json`, для того чтобы хранить эти данные на жестком диске, либо передавать по сети. Мы можем решить подобную задачу при помощи класса примесей и множественного наследования. Объявим класс `ExportJSON`, реализуем метод, который экспортирует данные в формате `json`, и создадим новый класс, который называется `ExDog`, и он будет наследоваться от класса \"собака\", `dog`, и нашего нового класса примесей, `ExportJSON`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "5ac468ac",
   "metadata": {},
   "outputs": [],
   "source": [
    "import json\n",
    "\n",
    "class ExportJSON:\n",
    "    def to_json(self):\n",
    "        return json.dumps(\n",
    "            {\n",
    "                \"name\": self.name,\n",
    "                \"breed\": self.breed\n",
    "            }\n",
    "        )\n",
    "\n",
    "class ExDog(Dog, ExportJSON):\n",
    "    pass"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "2a5229c2",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "{\"name\": \"\\u0411\\u0435\\u043b\\u043a\\u0430\", \"breed\": \"\\u0414\\u0432\\u043e\\u0440\\u043d\\u044f\\u0436\\u043a\\u0430\"}\n"
     ]
    }
   ],
   "source": [
    "dog = ExDog(\"Белка\", breed=\"Дворняжка\")\n",
    "print(dog.to_json())"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8c6e5c84",
   "metadata": {},
   "source": [
    "Созданный класс при помощи множественного наследования объединяет в себе свойства всех родительских классов. Если мы создадим объект `dog`, который будет являться экземпляром нашего нового класса `ExDog`, то мы сможем обратиться к методу `to_json`, который является методом примесей `ExportJSON`.\n",
    "\n",
    "С одной стороны, это кажется очень удобным и гибким, однако множественное наследование и использование большого количества примесей имеет ряд своих недостатков и минусов.\n",
    "\n",
    "Если у вас будет очень сложная иерархия классов и большое количество примесей, то код станет менее выразительным, и программисту, который разбирается с вашим кодом, будет достаточно тяжело его читать. Поэтому не стоит сильно увлекаться и создавать большое количество классов примесей.\n",
    "\n",
    "Любой класс в Python является потомком класса `object`. Мы можем легко убедиться в этом, если попробуем использовать функцию `issubclass`. Например, мы можем ее вызвать для класса `int`, и она вернет значение \"истина\". Также мы можем попробовать проверить, является ли наш класс `Dog`, \"собачка\", потомком класса `object` или потомком класса \"питомец\"."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "b5a24c8d",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "True"
      ]
     },
     "execution_count": 7,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "issubclass(int, object)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "365c1aec",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "True"
      ]
     },
     "execution_count": 8,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "issubclass(Dog, object)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "ed18b7da",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "True"
      ]
     },
     "execution_count": 9,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "issubclass(Dog, Pet)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "4a623d15",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "False"
      ]
     },
     "execution_count": 10,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "issubclass(Dog, int)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c6af4304",
   "metadata": {},
   "source": [
    "Однако наш класс \"собака\" не является потомком для класса `int`, и эта функция вернет значение \"ложь\". Также при помощи функции `isinstance` мы можем проверять, является ли конкретный объект экземпляром нужного нам класса, и, например, мы можем создать объект \"собака\" и проверить, является ли наш объект экземпляром класса `Dog`, `Pet` или `Object`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "id": "1bb8a142",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "True"
      ]
     },
     "execution_count": 11,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "isinstance(dog, Dog)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "id": "ac1239f4",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "True"
      ]
     },
     "execution_count": 12,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "isinstance(dog, Pet)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "id": "8bfa0602",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "True"
      ]
     },
     "execution_count": 13,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "isinstance(dog, object)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1c2b0b7f",
   "metadata": {},
   "source": [
    "Все эти три вызова вернут значение \"истина\". Вы можете использовать эти функции `issubclass`, `isinstance` в своем коде, они очень часто вам будут нужны.\n",
    "\n",
    "При помощи наследования Python позволяет выстраивать достаточно сложные иерархии классов. Несмотря на то, что мы создали небольшое количество классов, давайте взглянем на то, как выглядит наша полученная иерархия в итоге."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "id": "7c8d63fb",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "(__main__.ExDog, __main__.Dog, __main__.Pet, __main__.ExportJSON, object)"
      ]
     },
     "execution_count": 16,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "\"\"\"\n",
    "\n",
    "   object\n",
    "  /     \\\n",
    "Pet    ExportJSON\n",
    " |        /\n",
    "Dog      /\n",
    "   \\    /\n",
    "    ExDog\n",
    "\n",
    "\"\"\"\n",
    "\n",
    "ExDog.__mro__"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "821a6937",
   "metadata": {},
   "source": [
    "Так, у нас есть класс `ExDog`, который мы создали при помощи множественного наследования от класса `Dog` и класса примесей `ExportJSON`. В свою очередь, класс `Dog` наследуется от класса \"питомец\", и все остальные классы наследуются от класса `object`. Если мы попробуем создать экземпляр класса `ExDog` и обратиться к атрибуту `name`, то как же Python будет искать этот атрибут в существующей иерархии классов? Для этого в Python существует так называемый `Method Resolution Order`, или порядок разрешения методов, и он устроен не так просто, как вам бы могло показаться. \n",
    "\n",
    "Однако, и вообще, в целом это отдельная тема для изучения. Однако все, что вам нужно знать, это порядок, в котором Python ищет нужный атрибут или метод. Итак, у нас есть иерархия классов, связи, все родительские, прародительские классы, и обратиться к этому списку можно при помощи атрибута `__mro__`. Если мы попробуем обратиться к атрибуту `name`, то Python будет искать сначала в классе `ExDog`, затем `Dog`, и после того как он обратится к классу `Pet`, нужный атрибут `name` будет найден. Данный список еще называется линеаризацией класса, то есть Python любые атрибуты и методы ищет в этом списке линеаризации. Если он пройдется по всему списку и не найдет, то будет сгенерировано исключение `AttributeError`."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "68e92ddb",
   "metadata": {},
   "source": [
    "Про исключения мы с вами будем говорить позже. Теперь вы знаете, как Python ищет атрибуты и методы в сложной иерархии классов. Двигаемся дальше.\n",
    "\n",
    "В самом начале, когда мы создавали класс `Dog`, мы рассматривали вызов инициализатора базового класса. Это выглядит достаточно просто, мы просто вызываем `super` без параметров, и происходит вызов функции или метода из базового класса. Однако в Python можно обратиться не только к базовому классу, но и к любому методу в существующей иерархии. Как это делается? Давайте рассмотрим очередной пример."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a08baca6",
   "metadata": {},
   "outputs": [],
   "source": [
    "class ExDog(Dog, ExportJSON):\n",
    "    def __init__(self, name, breed=None):\n",
    "        super().__init__(name, breed)\n",
    "        # super(ExDog, self).__init__(name)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "59557161",
   "metadata": {},
   "source": [
    "Вызов функции `super` без параметров равносилен тому, если б мы указали сам класс и передали туда объект `self`. Опускать параметры очень удобно и зачастую вам часто именно так и придется делать. Однако если вам необходимо вызвать метод конкретного класса, то в функцию `super` надо передать его родителя. Хочу обратить ваше внимание, что именно нужно передавать родителя. Итак, если мы создадим новый класс `WoolenDog` и захотим обратиться к инициализатору класса \"питомец\", то нам необходимо в функции `super` указать класс \"родитель\". Если мы попробуем создать объект класса `Dog` и обратиться к атрибуту `breed`, то получим вывод, который показан на слайде."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "id": "25b98630",
   "metadata": {},
   "outputs": [],
   "source": [
    "class WoolenDog(Dog, ExportJSON):\n",
    "    def __init__(self, name, breed=None):\n",
    "        super(Dog, self).__init__(name)\n",
    "        self.breed = f\"Шерстяная собака породы {breed}\""
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "id": "d74097fc",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Шерстяная собака породы Такса\n"
     ]
    }
   ],
   "source": [
    "dog = WoolenDog(\"Жучка\", breed=\"Такса\")\n",
    "print(dog.breed)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ff956135",
   "metadata": {},
   "source": [
    "Тем самым, вы теперь знаете, как работает функция `super` и как можно обратиться к любому методу в сложной иерархии классов.\n",
    "\n",
    "Также в Python существуют приватные атрибуты. Давайте поговорим немного о них. Для того чтобы создать приватный атрибут, необходимо его имя записать через два символа нижнего подчёркивания. Предположим, мы атрибут `breed` или породу собак решили сделать приватным, объявили его следующим образом, тогда в самом классе к нему можно обращаться так же, а вот для классов-наследников этот атрибут будет уже недоступен."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "id": "fc92c066",
   "metadata": {},
   "outputs": [],
   "source": [
    "class Dog(Pet):\n",
    "    def __init__(self, name, breed=None):\n",
    "        super().__init__(name)\n",
    "        self.__breed = breed\n",
    "        \n",
    "    def say(self):\n",
    "        return f\"{self.name}: waw\"\n",
    "    \n",
    "    def get_breed(self):\n",
    "        return self.__breed"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b94634e4",
   "metadata": {},
   "source": [
    "Давайте попробуем выполнить код, который указан на слайде в Python ноутбуке. Для этого нам потребуется код этих классов. Давайте сделаем это. Итак, у нас есть класс \"Собака\", который унаследован от \"Питомца\". У класса \"Собаки\" есть приватный атрибут `breed` и обращение к нему. Итак, наш итоговый класс `ExDog`, в котором есть функция `get_breed`, и в ней записано обращение к приватному атрибуту суперкласса."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "id": "79743a39",
   "metadata": {},
   "outputs": [],
   "source": [
    "class ExDog(Dog, ExportJSON):\n",
    "    def get_breed(self):\n",
    "        return f\"Порода: {self.name} - {self.__breed}\""
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3b9ebc1f",
   "metadata": {},
   "source": [
    "Давайте попробуем создать объект нашего класса `ExDog`. Пусть собаку зовут \"Тузик\", и её порода будет \"питбуль\". Давайте теперь попробуем вызвать метод `get_breed` и попробуем получить её породу."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "id": "9dd3da45",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "{'name': 'Фокс', '_Dog__breed': 'Мопс'}"
      ]
     },
     "execution_count": 21,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "dog = ExDog(\"Фокс\", \"Мопс\")\n",
    "dog.__dict__"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "id": "cc812895",
   "metadata": {},
   "outputs": [
    {
     "ename": "AttributeError",
     "evalue": "'ExDog' object has no attribute '_ExDog__breed'",
     "output_type": "error",
     "traceback": [
      "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m",
      "\u001b[1;31mAttributeError\u001b[0m                            Traceback (most recent call last)",
      "\u001b[1;32m<ipython-input-22-c2424d891e87>\u001b[0m in \u001b[0;36m<module>\u001b[1;34m\u001b[0m\n\u001b[1;32m----> 1\u001b[1;33m \u001b[0mdog\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mget_breed\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m",
      "\u001b[1;32m<ipython-input-20-01db35386576>\u001b[0m in \u001b[0;36mget_breed\u001b[1;34m(self)\u001b[0m\n\u001b[0;32m      1\u001b[0m \u001b[1;32mclass\u001b[0m \u001b[0mExDog\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mDog\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mExportJSON\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m      2\u001b[0m     \u001b[1;32mdef\u001b[0m \u001b[0mget_breed\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mself\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m----> 3\u001b[1;33m         \u001b[1;32mreturn\u001b[0m \u001b[1;34mf\"Порода: {self.name} - {self.__breed}\"\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m",
      "\u001b[1;31mAttributeError\u001b[0m: 'ExDog' object has no attribute '_ExDog__breed'"
     ]
    }
   ],
   "source": [
    "dog.get_breed()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a685d675",
   "metadata": {},
   "source": [
    "Как мы видим, у нас не получилось это сделать, возникло исключение `AttributeError`. Давайте попробуем разобраться, в чём же дело. Можно распечатать внутренний атрибут `__dict__`, который нам покажет все атрибуты нашего созданного объекта. Итак, мы видим, что вместо атрибута `__breed` Python автоматически поставил имя класса. \n",
    "\n",
    "В целом, если нам очень нужно обратиться к этому приватному атрибуту, Python позволяет это сделать, и мы можем исправить код нашего класса, добавить префикс с классом Dog и попробовать ещё раз обратиться к нему."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 23,
   "id": "3076e8c5",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'Порода: Фокс - Мопс'"
      ]
     },
     "execution_count": 23,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "class ExDog(Dog, ExportJSON):\n",
    "    def get_breed(self):\n",
    "        return f\"Порода: {self.name} - {self._Dog__breed}\"\n",
    "    \n",
    "dog = ExDog(\"Фокс\", \"Мопс\")\n",
    "dog.get_breed()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6bac3f91",
   "metadata": {},
   "source": [
    "Итак, у нас получилось. То есть, как я уже сказал, Python позволяет обращаться к приватным атрибутам класса вне самого класса, однако, лучше сильно этим не увлекаться.\n",
    "\n",
    "Мы обсудили с вами то, как устроено наследование классов в Python, мы поговорили про множественное наследование, обсудили, как Python ищет атрибуты и методы в сложной иерархии классов. Также поговорили о вызове методов при помощи функции `super` в сложной иерархии классов и обсудили приватные атрибуты или `name mangling`. \n",
    "\n",
    "На следующей лекции мы обсудим ещё один интересный подход, который называется композицией, и сравним его с наследованием."
   ]
  }
 ],
 "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
}
