{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "cbc60620",
   "metadata": {},
   "source": [
    "# Классы и экземпляры #"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8a6d683a",
   "metadata": {},
   "source": [
    "На этой лекции мы начнем знакомиться с классами и с экземплярами классов.\n",
    "\n",
    "Надеюсь, вы, так или иначе, сталкивались с объектно-ориентированным программированием, когда работали с другими языками программирования. Но, даже если нет, ничего страшного. Примеры, которые я буду показывать, достаточно просты, чтобы вы смогли понять.\n",
    "\n",
    "На самом деле, в объектно-ориентированном программировании ничего сложного нет. По сути, это просто вопрос организации кода. \n",
    "\n",
    "Вообще, зачем нужны классы? Часто, когда говорят о классах, говорят, что их используют тогда, когда нужно отобразить реальные предметы, вещи на программный код. Отчасти это так, но я бы сказал, что классы нужны для того, чтобы объединить функционал какой-то, связанный общей идеей и смыслом, в одну сущность, причем у этой сущности может быть свое внутреннее состояние, а также методы, которые позволяют это состояние модифицировать.\n",
    "\n",
    "Про методы класса мы также с вами будем говорить. Какие можно привести реальные примеры использования классов? Например, это может быть обертка над соединением к базе данных. У нас есть класс, в котором есть состояние. И это состояние — это постоянное TCP соединение с базой. Также у этого класса могут быть методы, которые предоставляют некий интерфейс доступа к этому TCP соединению. Методы представляют собой, например, возможности выбрать какие-то данные из базы, либо положить какие-то данные в базу. Тем самым мы инкапсулируем, то есть скрываем, TCP соединение внутри класса, а пользователю нашего класса предоставляем удобный интерфейс доступа к данным.\n",
    "\n",
    "Также, например, классы хорошо ложатся на реализацию всевозможных игр. Например, давайте подумаем о игре жанра RPG (role-playing game). Там есть квесты, монстры, игроки, предметы инвентаря — все это со своими свойствами и возможностями. Классы позволяют все это реализовать в программном коде. Давайте начнем знакомиться с классами, и прежде всего, откроем интерактивный интерпретатор Python, запустив консоль, набрав Python 3, и посмотрим.\n",
    "\n",
    "Встроенные типы, о которых мы рассказывали вам на первой неделе, на самом деле являются классами в Python'е. Я набрал `int`, и видим, что это класс `int`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "1712fb7d",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "int"
      ]
     },
     "execution_count": 8,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "int"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d5cc4612",
   "metadata": {},
   "source": [
    "То же самое, например, с классом `float`. На второй неделе мы знакомили вас со структурами данных, такими как, например, `dict` — словарь, или список — `list`. Это тоже классы. Когда мы создаем переменную и присваиваем ей число, например, `13`, мы, на самом деле, создаем объект класса `int` и можем с ним работать.\n",
    " \n",
    "Посмотрим тип нашей переменной — это как раз класс `int`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "90641e72",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "int"
      ]
     },
     "execution_count": 9,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "num = 13\n",
    "type(num)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "14509c8d",
   "metadata": {},
   "source": [
    "В Python есть встроенная функция `isinstance`. `isintance` — это функция, которая позволяет в рантайме посмотреть, удовлетворяет ли какой-то объект какому-либо классу, типу. Давайте попробуем вызвать эту функцию и проверим, что переменная `num`, которой мы присвоили число 13, является объектом класса `int`. Это `True`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "e8a30b65",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "True"
      ]
     },
     "execution_count": 10,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "isinstance(num, int)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "589be9aa",
   "metadata": {},
   "source": [
    "Если бы мы попробовали проверить, что это `float`, мы увидели бы, что это `False`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "id": "3ee4dd3a",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "False"
      ]
     },
     "execution_count": 12,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "isinstance(num, float)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "74793a85",
   "metadata": {},
   "source": [
    "Как мы видим, классы есть и в стандартной библиотеке Python'а. Давайте посмотрим, как реализовывать и писать свои собственные классы.\n",
    "\n",
    "Чтобы объявить класс, мы используем ключевое слово `class`, за которым следует название класса.\n",
    "\n",
    "Классы в Python'е принято называть `CamelCase`'ом, то есть с большой буквы. После этого мы ставим двоеточие и дальше идет блок класса, блок пространства имен класса. В данном случае мы используем ключевое слово `pass`. Помните, на первой неделе я говорил, что ключевое слово `pass` может использоваться для определения класса, который ничего не делает? Вот это как раз тот случай. В данном случае мы объявили класс `Human`, который пока ничего не умеет."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "id": "7072ec4c",
   "metadata": {},
   "outputs": [],
   "source": [
    "class Human:\n",
    "    pass"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d383e670",
   "metadata": {},
   "source": [
    "Есть другой способ определить пустой класс, который ничего не делает — используя `docstring`. Мы пишем имя класса, и в блоке класса мы просто пишем строку документирования. Это также валидный синтаксис на Python."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "id": "b4956140",
   "metadata": {},
   "outputs": [],
   "source": [
    "class Robot:\n",
    "    \"\"\"Данный класс позволяет создавать роботов\"\"\""
   ]
  },
  {
   "cell_type": "markdown",
   "id": "26607708",
   "metadata": {},
   "source": [
    "Теперь, если мы посмотрим, что представляет собой объект `Robot`, мы увидим, что это класс \"__main__.Robot\"."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "id": "083bd8f8",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "<class '__main__.Robot'>\n"
     ]
    }
   ],
   "source": [
    "print(Robot)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "842eb9ef",
   "metadata": {},
   "source": [
    "`main` в данном случае — это имя модуля, в котором он определен, мы ничего не импортировали, никаких модулей не создавали, поэтому наш модуль — это модуль `main`. Дальше мы видим названия класса. Если мы посмотрим на то, какие есть методы у этого объекта, только что созданного — у класса `Robot`, — то мы увидим, что их на самом деле много."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "id": "85abcf2a",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__']\n"
     ]
    }
   ],
   "source": [
    "print(dir(Robot))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a2439475",
   "metadata": {},
   "source": [
    "Про некоторые из них мы с вами поговорим, а пока давайте поговорим о том, как создавать экземпляры или, как по-другому говорят, объекты класса. Предположим, что у нас есть класс `Planet` — класс, который описывает какую-либо планету, но этот класс описывает абстрактную планету, то есть он описывает все планеты в целом. Когда мы работаем с планетами, нас будут зачастую интересовать именно конкретные экземпляры планет, то есть не какая-то абстрактная планета, а, например, Земля, Марс и так далее."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "id": "10bfe179",
   "metadata": {},
   "outputs": [],
   "source": [
    "class Planet:\n",
    "    pass"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "86ee64b4",
   "metadata": {},
   "source": [
    "Для того, чтобы создать экземпляр класса, мы обращаемся к имени класса как к функции — используем синтаксис круглых скобочек — и получаем объект, либо экземпляр, класса."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "id": "a64bd9a2",
   "metadata": {},
   "outputs": [],
   "source": [
    "planet = Planet()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "04a94495",
   "metadata": {},
   "source": [
    "Это мы можем посмотреть на то, что нам печатает Python, когда мы принтим только что созданную переменную, которая связана с объектом класса."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "id": "19a05a52",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "<__main__.Planet object at 0x03588418>\n"
     ]
    }
   ],
   "source": [
    "print(planet)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1a9a23fa",
   "metadata": {},
   "source": [
    "И мы видим, что это `__main__.Planet object`, то есть это уже объект, а не просто класс, и далее идет адрес, по которому этот объект располагается в памяти."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "623bad31",
   "metadata": {},
   "source": [
    "С классами можно работать точно так же, как и с другими вещами в Python, так как все в Python есть объект, как вы знаете. Ничто нам не мешает оперировать классами, как любыми другими вещами. Например, давайте посмотрим на слайд, и мы видим, что на этом слайде мы создали список, который называется \"Солнечная система\".\n",
    "\n",
    "Мы знаем, что в нашей Солнечной системе есть 8 планет. Давайте проитерируемся от 0 до 8 невключительно, и создадим 8 планет, и добавим эти планеты в список. Мы видим, что у нас получилось. Наш финальный список Солнечной системы содержит как раз 8 планет."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "id": "b8c1ada6",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[<__main__.Planet object at 0x03588658>, <__main__.Planet object at 0x03588418>, <__main__.Planet object at 0x035886D0>, <__main__.Planet object at 0x035886E8>, <__main__.Planet object at 0x03588700>, <__main__.Planet object at 0x03588718>, <__main__.Planet object at 0x03588730>, <__main__.Planet object at 0x03588748>]\n"
     ]
    }
   ],
   "source": [
    "solar_system = []\n",
    "for i in range(8):\n",
    "    planet = Planet()\n",
    "    solar_system.append(planet)\n",
    "    \n",
    "print(solar_system)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ff0890b4",
   "metadata": {},
   "source": [
    "Что важно отметить? Важно отметить, что экземпляры класса хешируются, то есть экземпляры классов могут быть ключами словаря. На слайде вы видите точно такой же пример, как и предыдущий, за исключением того, что теперь мы в качестве Солнечной системы используем словарь."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "id": "3ed4eb38",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "{<__main__.Planet object at 0x03588400>: True, <__main__.Planet object at 0x03588748>: True, <__main__.Planet object at 0x035886E8>: True, <__main__.Planet object at 0x03588700>: True, <__main__.Planet object at 0x03588718>: True, <__main__.Planet object at 0x03588730>: True, <__main__.Planet object at 0x03588628>: True, <__main__.Planet object at 0x03588670>: True}\n"
     ]
    }
   ],
   "source": [
    "solar_system = {}\n",
    "for i in range(8):\n",
    "    planet = Planet()\n",
    "    solar_system[planet] = True\n",
    "    \n",
    "print(solar_system)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9ddfd1c1",
   "metadata": {},
   "source": [
    "Однако не все так хорошо. Наши планеты, которые мы объявили, то есть создали экземпляры класса Planet, они не имеют имени. Это не очень удобно, с этим сложно работать. У каждой планеты должно быть свое имя. Для того чтобы дать планете имя, мы должны переопределить инициализатор класса.\n",
    "\n",
    "В Python'е у классов существует очень много магических методов. Но самый главный, по сути, магический метод — это метод `__init__` с двумя подчеркиваниями в начале и в конце. Метод `init` позволяет переопределить инициализацию класса. Чтобы вы поняли, давайте посмотрим это на примере."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 23,
   "id": "5408c883",
   "metadata": {},
   "outputs": [],
   "source": [
    "class Planet:\n",
    "    def __init__(self, name):\n",
    "        self.name = name"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "13666ce4",
   "metadata": {},
   "source": [
    "Мы переопределили метод `__init__`. Про то, как он представлен, мы скоро с вами поговорим."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 24,
   "id": "8dac9ceb",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Earth\n",
      "<__main__.Planet object at 0x03588040>\n"
     ]
    }
   ],
   "source": [
    "earth = Planet(\"Earth\")\n",
    "print(earth.name)\n",
    "print(earth)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "50c798e6",
   "metadata": {},
   "source": [
    "Но давайте посмотрим. Мы объявляем переменную `Earth`, которая является экземпляром класса `Planet`. После этого вызывается метод `__init__`. Метод `__init__` вызывается автоматически Python'ом при создании экземпляра. Первым аргументом метод `__init__` принимает ссылку на только что созданный экземпляр класса. Он еще не проинициализирован — как раз метод `__init__` его инициализирует. Также он после `self` принимает те аргументы, которые мы передаем, обращаясь к классу, когда мы создаем экземпляр. То есть вот в данном случае на место `name` подставится строка `Earth`. Внутри инициализатора мы можем по ссылке `self` установить так называемые атрибуты экземпляра. В данном случае мы ставим атрибут экземпляра `name` и присваиваем его аргументу `name`.\n",
    "\n",
    "Теперь у нас у каждой планеты есть свое имя. Мы можем обратиться к имени планеты, а точнее, к атрибуту экземпляра, используя точку, то есть `имя переменной.name`. И мы видим, что на экран напечатается `Earth`. Также, если мы сейчас посмотрим и напечатаем наш класс, то мы увидим, что на экран выведется вот такое вот описание, то есть это объект класса `Planet`.\n",
    "\n",
    "А что если мы хотим, чтобы когда мы печатаем нашу планету, Python печатал ее имя? На самом деле, для этого существует еще один магический метод — метод `__str__` с двумя подчеркиваниями в начале и в конце. Он позволяет нам переопределить то, как будет печататься объект. В данном случае мы возвращаем имя нашей планеты и теперь, когда мы печатаем на экран наш экземпляр, используя функцию `print`, на экран выводится имя нашей планеты."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 25,
   "id": "6f57adcd",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Earth\n"
     ]
    }
   ],
   "source": [
    "class Planet:\n",
    "    def __init__(self, name):\n",
    "        self.name = name\n",
    "        \n",
    "    def __str__(self):\n",
    "        return self.name\n",
    "    \n",
    "    \n",
    "earth = Planet(\"Earth\")\n",
    "print(earth)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ca8fafa0",
   "metadata": {},
   "source": [
    "Вернемся к примеру, где мы создаем Солнечную систему. Теперь мы населяем Солнечную систему планетами со своим именем. Однако, обратите внимание, что, несмотря на то, что мы переопределили метод `__str__`, магические классы, мы внутри списка видим объекты, которые печатаются по-прежнему в том представлении, которое печаталось до этого, до переопределения `__str__`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 26,
   "id": "51071ecf",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[<__main__.Planet object at 0x03588730>, <__main__.Planet object at 0x03588670>, <__main__.Planet object at 0x03588748>, <__main__.Planet object at 0x03588928>, <__main__.Planet object at 0x03588958>, <__main__.Planet object at 0x03588988>, <__main__.Planet object at 0x035889B8>, <__main__.Planet object at 0x035889E8>]\n"
     ]
    }
   ],
   "source": [
    "solar_system = []\n",
    "planet_names = [\n",
    "    \"Mercury\", \"Venus\", \"Earth\", \"Mars\",\n",
    "    \"Jupiter\", \"Saturn\", \"Uranus\", \"Neptune\"\n",
    "]\n",
    "\n",
    "\n",
    "for name in planet_names:\n",
    "    planet = Planet(name)\n",
    "    solar_system.append(planet)\n",
    "    \n",
    "print(solar_system)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "86c8e29e",
   "metadata": {},
   "source": [
    "Дело в том, что в данном случае Python использует другой магический метод, когда мы внутри списка, то есть встроенный, другой магический метод — метод `__repr__` с двумя подчеркиваниями, который мы также имеем возможность переопределить для того, чтобы и внутреннее представление объекта также печаталось правильно в данном случае."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 28,
   "id": "7332eb54",
   "metadata": {},
   "outputs": [],
   "source": [
    "class Planet:\n",
    "    def __init__(self, name):\n",
    "        self.name = name\n",
    "        \n",
    "    def __repr__(self):\n",
    "        return f\"Planet {self.name}\""
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f0f43524",
   "metadata": {},
   "source": [
    "Если мы переопределим `__repr__` и выполним тот же самый пример, то мы уже увидим, что внутри нашего списка планеты названы правильно."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 29,
   "id": "5f2b26d4",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[Planet Mercury, Planet Venus, Planet Earth, Planet Mars, Planet Jupiter, Planet Saturn, Planet Uranus, Planet Neptune]\n"
     ]
    }
   ],
   "source": [
    "solar_system = []\n",
    "planet_names = [\n",
    "    \"Mercury\", \"Venus\", \"Earth\", \"Mars\",\n",
    "    \"Jupiter\", \"Saturn\", \"Uranus\", \"Neptune\"\n",
    "]\n",
    "\n",
    "\n",
    "for name in planet_names:\n",
    "    planet = Planet(name)\n",
    "    solar_system.append(planet)\n",
    "    \n",
    "print(solar_system)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bd1e1eec",
   "metadata": {},
   "source": [
    "Магических методов, на самом деле, существует масса, и мы будем вас знакомить с другими магическими методами. \n",
    "\n",
    "Например, есть метод `__add__` с двумя подчеркиваниями, который позволяет переопределить, как ведут себя два объекта класса, когда мы их складываем друг с другом, и так далее.\n",
    "\n",
    "Но многие из них мы даже не сможем осветить в рамках нашего курса, но вы всегда сможете посмотреть это в документации.\n",
    "\n",
    "Теперь давайте вернемся к атрибутам экземпляра и посмотрим, как с ними можно работать. Когда мы создаем экземпляр класса `Planet`, мы можем обратиться к атрибуту через точку, как я уже показывал. Однако это не все — мы можем в любой момент изменить атрибут экземпляра, присвоив ему другое значение. Например, как здесь на слайде показано, мы присвоили атрибуту экземпляра `name` другое значение `Second Earth`, и оно меняется."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 30,
   "id": "aaf6b1a2",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Planet Mars\n"
     ]
    }
   ],
   "source": [
    "mars = Planet(\"Mars\")\n",
    "print(mars)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 31,
   "id": "76ad2d40",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'Mars'"
      ]
     },
     "execution_count": 31,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "mars.name"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 33,
   "id": "143730ea",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'Second Earth?'"
      ]
     },
     "execution_count": 33,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "mars.name = \"Second Earth?\"\n",
    "mars.name"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ce6886d1",
   "metadata": {},
   "source": [
    "Если мы обратимся к несуществующему атрибуту экземпляра, то вызовется `Exception AttributeError`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 34,
   "id": "8c93cba5",
   "metadata": {},
   "outputs": [
    {
     "ename": "AttributeError",
     "evalue": "'Planet' object has no attribute 'mass'",
     "output_type": "error",
     "traceback": [
      "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m",
      "\u001b[1;31mAttributeError\u001b[0m                            Traceback (most recent call last)",
      "\u001b[1;32m<ipython-input-34-318658eaebb0>\u001b[0m in \u001b[0;36m<module>\u001b[1;34m\u001b[0m\n\u001b[1;32m----> 1\u001b[1;33m \u001b[0mmars\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mmass\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m",
      "\u001b[1;31mAttributeError\u001b[0m: 'Planet' object has no attribute 'mass'"
     ]
    }
   ],
   "source": [
    "mars.mass"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "fa124450",
   "metadata": {},
   "source": [
    "Про то, как отлавливать такие ошибки, мы с вами будем говорить в этом блоке. Также, если мы удалим какой-нибудь атрибут экземпляра, используя конструкцию `del` — мы можем это сделать, — то, обратившись потом к `mars.name`, мы также получим `AttributeError`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 35,
   "id": "93b88088",
   "metadata": {},
   "outputs": [],
   "source": [
    "del mars.name"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 36,
   "id": "e4a86397",
   "metadata": {},
   "outputs": [
    {
     "ename": "AttributeError",
     "evalue": "'Planet' object has no attribute 'name'",
     "output_type": "error",
     "traceback": [
      "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m",
      "\u001b[1;31mAttributeError\u001b[0m                            Traceback (most recent call last)",
      "\u001b[1;32m<ipython-input-36-373d1c5d2a16>\u001b[0m in \u001b[0;36m<module>\u001b[1;34m\u001b[0m\n\u001b[1;32m----> 1\u001b[1;33m \u001b[0mmars\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mname\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m",
      "\u001b[1;31mAttributeError\u001b[0m: 'Planet' object has no attribute 'name'"
     ]
    }
   ],
   "source": [
    "mars.name"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c4484539",
   "metadata": {},
   "source": [
    "Давайте подведем итог. Мы с вами посмотрели, как объявлять классы, объявлять классы, которые ничего по большому счету не делают, то есть это просто объявления в пространстве имен класса, который следует после объявления. Мы научились создавать экземпляры классов, или по-другому, объекты классов. Мы рассмотрели, как инициализировать экземпляр класса, то есть как наделять его конкретными атрибутами, чтобы как-то делать наши объекты уникальными — например, как мы помним, дать уникальное имя каждой из планет. Также мы научились работать с атрибутами экземпляра классов, то есть мы можем создавать их, читать и удалять."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3c640f57",
   "metadata": {},
   "source": [
    "Давайте продолжим говорить о классах и экземплярах. И сейчас мы посмотрим на то, что такое атрибуты класса. \n",
    "\n",
    "Иногда вам нужно создать переменную, которая будет работать в контексте класса, но которая не связана с каждым конкретным экземпляром. То есть, эта переменная относится непосредственно к самому классу, а не к экземпляру. Такая переменная называется атрибутом класса и на слайде вы можете видеть ее."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 37,
   "id": "0c52ac87",
   "metadata": {},
   "outputs": [],
   "source": [
    "class Planet:\n",
    "    count = 0\n",
    "    \n",
    "    def __init__(self, name, population=None):\n",
    "        self.name = name\n",
    "        self.population = population or []\n",
    "        Planet.count += 1"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1462d08e",
   "metadata": {},
   "source": [
    "Вот этот атрибут класса, атрибут, который мы назвали `сount`. В данном случае мы в методе `__init__` для каждого экземпляра увеличиваем количество планет, которое было создано, обращаясь к атрибуту класса через имя класса.\n",
    "\n",
    "На практике это может быть иногда полезно. Давайте посмотрим, как это сделать. Мы объявляем две переменных `Earth` и `Mars`. Это два экземпляра класса `Planet`. И после этого, когда мы обращаемся к атрибуту `сount`, несмотря на то, что атрибут `сount` не является атрибутом экземпляра, мы получаем его значение."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 38,
   "id": "e1746036",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "2\n"
     ]
    }
   ],
   "source": [
    "earth = Planet(\"Earth\")\n",
    "mars = Planet(\"Mars\")\n",
    "\n",
    "print(Planet.count)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6493afcc",
   "metadata": {},
   "source": [
    "Мы также можем обратиться к атрибуту `сount` через объект экземпляр класса и также получим его значение."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 39,
   "id": "566c2441",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "2"
      ]
     },
     "execution_count": 39,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "mars.count"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1b02b524",
   "metadata": {},
   "source": [
    "В этот момент Python видит, что внутри экземпляра класса такого атрибута нет, и он проверяет сам класс на наличие атрибута класса и находит его. На самом деле, нужно быть осторожным, когда используешь атрибуты класса. Часто может возникнуть путаница, когда мы хотим обращаться к атрибуту как к атрибуту, привязанному к конкретному экземпляру, а на самом деле это атрибут класса. Но за этим нужно следить. Это достаточно неявно, но это не так сложно.\n",
    "\n",
    "Деструктор экземпляра класса. Что это такое? Когда счетчик ссылок на экземпляр класса достигает 0, помните, мы уже с вами говорили про сборщик мусора в Python и то, что он использует счетчик ссылок, вызывается метод `__del__` экземпляра. Это также магический метод, который Python нам предоставляет возможность переопределить. В данном случае мы переопределяем этот метод для экземпляра и печатаем на экран слово \"goodbye\"."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 40,
   "id": "ca3eabaf",
   "metadata": {},
   "outputs": [],
   "source": [
    "class Human:\n",
    "    def __del__(self):\n",
    "        print(\"Goodbye!\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2f4f8fc2",
   "metadata": {},
   "source": [
    "Если мы создадим экземпляр класса, а затем воспользуемся конструкцией `__del__`, чтобы удалить, в этот момент вызовется как раз этот магический метод `__del__` и напечатается \"goodbye\"."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 41,
   "id": "4a2284b5",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Goodbye!\n"
     ]
    }
   ],
   "source": [
    "human = Human()\n",
    "# в данном случае деструктор отработает - но все же\n",
    "# лучше создать метод и вызывать его явно\n",
    "del human"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1c895418",
   "metadata": {},
   "source": [
    "Однако, на практике лучше магический метод `__del__` не переопределять, потому что нет гарантии, что, по завершению работы интерпретатора Python, он будет вызван. Лучше явно определить метод, который будет выполнять какие-то действия, которые вам нужны. Какие вообще это могут быть действия? Например, вы можете закрыть файл в методе `__del__` , либо разорвать сетевое соединение. Так вот, эти действия лучше описывать явно в методах экземпляра, о которых мы будем говорить с вами в следующей лекции.\n",
    "\n",
    "Словарь экземпляра и класса. Давайте посмотрим на пример и увидим, что это знакомый нам класс `Planet`, у которого переопределен метод `__init__`, у которого ставятся атрибуты `name` и `population` (население планеты). А также есть атрибут класса. Если мы посмотрим на свойство `__dict__` у экземпляра класса `Planet`, то мы увидим, что это словарь, у которого есть как раз атрибуты нашего класса `name` и `population`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 42,
   "id": "906ab5e6",
   "metadata": {},
   "outputs": [],
   "source": [
    "class Planet:\n",
    "    \"\"\"This class describes planets\"\"\"\n",
    "    count = 1\n",
    "    def __init__(self, name, population=None):\n",
    "        self.name = name\n",
    "        self.population = population or []\n",
    "        \n",
    "planet = Planet(\"Earth\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 44,
   "id": "c343d912",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "{'name': 'Earth', 'population': []}"
      ]
     },
     "execution_count": 44,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "planet.__dict__"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ed54e0d8",
   "metadata": {},
   "source": [
    "Когда мы обращаемся к каким-то атрибутам, Python, в первую очередь, смотрит в эту структуру данных, в этот словарь и ищет там эти атрибуты. Если мы добавляем атрибут экземпляру нашего класса, в данном случае на слайде мы добавляем массу планете Земля, то в словаре экземпляра класса появляется этот атрибут. Соответственно, в следующий раз когда мы пытаемся получить к нему доступ, Питон находит этот атрибут в этом словаре и выдает нам его значение."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 45,
   "id": "a41571c0",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "{'name': 'Earth', 'population': [], 'mass': 5.97e+24}"
      ]
     },
     "execution_count": 45,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "planet.mass = 5.97e24\n",
    "planet.__dict__"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7d8f98c1",
   "metadata": {},
   "source": [
    "Если мы посмотрим на атрибут `__dict__` у, непосредственно, класса, а не у экземпляра, то мы увидим, что он также присутствует."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 46,
   "id": "419f8b35",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "mappingproxy({'__module__': '__main__',\n",
       "              '__doc__': 'This class describes planets',\n",
       "              'count': 1,\n",
       "              '__init__': <function __main__.Planet.__init__(self, name, population=None)>,\n",
       "              '__dict__': <attribute '__dict__' of 'Planet' objects>,\n",
       "              '__weakref__': <attribute '__weakref__' of 'Planet' objects>})"
      ]
     },
     "execution_count": 46,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "Planet.__dict__"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5f9fd4b2",
   "metadata": {},
   "source": [
    "И это объект `mapping proxy`, который похож на словарь, у которого также есть множество всевозможных ключей, в том числе, это ключ `__doc__`. Это тот `docstring`, который мы прописали нашему классу. Также, обратите внимание, что именно в словаре самого класса присутствует атрибут класса `сount`.\n",
    "\n",
    "Если мы обратимся к свойству `__doc__`, например, то это свойство будет найдено в этом словаре и Python вернет нам его значение."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 47,
   "id": "2fd32d64",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'This class describes planets'"
      ]
     },
     "execution_count": 47,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "Planet.__doc__"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c80b7e08",
   "metadata": {},
   "source": [
    "Как я уже говорил, мы можем обращаться к свойствам, которые есть в словаре класса и от экземпляра конкретного. То есть, если мы обратимся к свойству `__doc__` конкретного экземпляра `Planet`, который является объектом класса `Planet`, мы также увидим на экране нашу строку."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 48,
   "id": "534389c5",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'This class describes planets'"
      ]
     },
     "execution_count": 48,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "planet.__doc__"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e4334801",
   "metadata": {},
   "source": [
    "Если мы посмотрим, какие еще есть методы у экземпляра класса, мы увидим, что их, на самом деле, очень много. Например, есть `__hash__()`. Как я говорил, экземпляры класса хешируемые и могут быть использованы в ключах словаря. А также есть очень много других магических методов, про которые мы с вами еще поговорим."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 49,
   "id": "471a73e8",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'count', 'mass', 'name', 'population']\n"
     ]
    }
   ],
   "source": [
    "print(dir(planet))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3f043b02",
   "metadata": {},
   "source": [
    "Также в любой момент мы от экземпляра можем получить класс, к которому он принадлежит, используя свойство `__class__`. И видим, что `planet.__class__` нам возвращает `__main__.planet`, наш класс."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 50,
   "id": "23751219",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "__main__.Planet"
      ]
     },
     "execution_count": 50,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "planet.__class__"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d935d836",
   "metadata": {},
   "source": [
    "И последнее, на что мы посмотрим, это на конструктор экземпляра класса. Давайте посмотрим на пример."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 51,
   "id": "a95f1d97",
   "metadata": {},
   "outputs": [],
   "source": [
    "class Planet:\n",
    "    def __new__(cls, *args, **kwargs):\n",
    "        print(\"__new__ called\")\n",
    "        obj = super().__new__(cls)\n",
    "        return obj\n",
    "    \n",
    "    def __init__(self, name):\n",
    "        print(\"__init__ called\")\n",
    "        self.name = name"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4c4bac50",
   "metadata": {},
   "source": [
    "Конструктор экземпляра класса позволяет нам переопределить какие-то действия, которые происходят с экземпляром до его инициализации. На самом деле, на практике это встречается нечасто, но знать об этом нужно. В следующих лекциях я вам покажу пример использования магического метода `__new__`, который как раз является конструктором экземпляра класса, в рамках использования метаклассов. А сейчас давайте просто посмотрим на то, в каком порядке вызывается магический метод `__new__`, который мы сейчас переопределим, и магический метод `__init__`, который инициализирует класс."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0950376b",
   "metadata": {},
   "source": [
    "Здесь на примере мы переопределили магический метод `__new__`, который принимает первым аргументом класс, это как раз в нашем случае класс `Planet`. И здесь мы сделали то же самое, что делает `Python`, ничего не изменив, за исключением того, что добавили `print`, что был вызван метод `__new__`. Давайте посмотрим, что значит вот эта строка. Вот эта строка - это как создание нового экземпляра класса, который еще не инициализирован. Метод `super` возвращает родителя нашего класса, в данном случае это `Object`. Это класс, от которого наследуются все пользовательские классы в Python 3. Мы вызываем метод `__new__` класса `Object`. И нам возвращается экземпляр. Этот экземпляр мы просто возвращаем."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 52,
   "id": "db748979",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "__new__ called\n",
      "__init__ called\n"
     ]
    }
   ],
   "source": [
    "earth = Planet(\"Earth\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "68c3d35c",
   "metadata": {},
   "source": [
    "Когда мы создаем экземпляр класса `Planet` , мы видим, что сначала вызывается метод `__new__`, а затем вызываем метод `__init__`. То есть, сначала создается экземпляр, а затем он инициализируется. То есть, если переложить это на код, происходит примерно следующее: сначала вызывается метод `__new__`, который получает на вход класс, в данном случае `Planet`. А вторым аргументом то, что мы передаем при создании экземпляра, наши аргументы, в данном случае строка `Earth`. И дальше, если конструктор `__new__` вернул нам правильный класс, если он, соответствуя типу, с помощью функции `isinstance` проверяет Python, то вызывается метод `__init__`. То есть, происходит инициализация нашего объекта, используя те аргументы, которые мы передавали для того, чтобы создать экземпляр."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 53,
   "id": "ac5ef6fe",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "__new__ called\n",
      "__init__ called\n"
     ]
    }
   ],
   "source": [
    "planet = Planet.__new__(Planet, \"Earth\")\n",
    "\n",
    "if isinstance(planet, Planet):\n",
    "    Planet.__init__(planet, \"Earth\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a365bf73",
   "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.8.8"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
