{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "7749287c",
   "metadata": {},
   "source": [
    "# Пример на классы #"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3efae2d7",
   "metadata": {},
   "source": [
    "Теперь от теории классов давайте перейдем к практике.\n",
    "\n",
    "Мы напишем небольшую программку, причем напишем её с нуля, с самого начала и эта программа будет содержать класс.\n",
    "\n",
    "Класс будет принимать на вход название города и будет иметь метод, который будет возвращать прогноз погоды в этом городе.\n",
    "\n",
    "На самом деле этот класс можно будет расширять бесконечно, например добавляя ему методы для получения грядущих событий в вашем любимом городе либо новостей относящихся к нему.\n",
    "\n",
    "Давайте начнём."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "69e00a67",
   "metadata": {},
   "source": [
    "Первое что мы сделаем, это создадим директорию в которой будем работать, перейдем в неё, создадим virtual env - вы уже умеете это делать. После того, как виртуальное окружение создастся мы должны его активировать. Нам понадобятся две сторонние библиотеки для работы нашей программы. Давайте их установим. Первая из них это библиотечка `requests`, а вторая это `python-dateutil`. `Request` нам потребуется чтобы делать `http` запросы, а `Python-dateutil` позволит преобразовывать даты, которые представлены в виде строки в Python'овское представление, то есть, в объекты модуля `datetime`. У нас установились наши библиотеки."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2c37bb80",
   "metadata": {},
   "source": [
    " Сейчас я начну программировать в среде программирования `Visual Studio Code` или по другому говоря `VSCode`. Это новая для вас среда, возможно многие из вас с ней не знакомы. Вот собственно сейчас мы с ней и познакомимся. \n",
    "\n",
    "\n",
    "Внутри `VSCode`'а мы откроем нашу директорию в которой мы только что создали виртуальное окружение. Это делается с помощью `file - open`. Открываем нашу директорию и вот она открылась. Мы видим наше виртуальное окружение. Следующим шагом мы должны указать, где же находится интерпретатор Python для нашего проекта. `Select wordspace interpreter`. Выбираем интерпретатор Python из нашего виртуального окружения. Внутри нашего проекта мы можем создать файлик. Назовем его `city.py`. Напомню что наша программка будет выдавать прогноз погоды в городе. Как только мы создали файл, `Visual Studio Code` предлагает нам установить `linter`, то есть специальные утилиты, которые помогают нам программировать. В данном случае мы установим `linter pep8` чтобы он следил за стилем нашего кода, а также `linter flake8`. Он будет помогать нам находить очевидные ошибки в нашем коде. `Pylint` мы не будем устанавливать, но это также замечательный `linter`. Теперь мы наконец-то можем программировать. С чего же нам начать? Давайте начнем со стандартного шага. Напишем `if __name__ = \"__main__\"`, то есть будем запускать нашу программу только тогда, когда она вызывается напрямую. А не импортируется. Внутри этой конструкции вызовем функцию `_main`. Назовем её начиная с символа нижнего подчеркивания, чтобы подчеркнуть то, что она является приватной и не должна использоватся из стороннего кода. Объявим эту функцию. Нам нужен класс, который будет принимать на вход имя города. Давайте начнем программу писать с конца. То есть не имея ещё класса объявим скелет нашей программы. Будем смотреть погоду в Москве."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c918e4d0",
   "metadata": {},
   "source": [
    "У экземпляра класса `CityInfo` будет метод `weather_forecast`. Этот метод вернет нам список с прогнозом погоды на несколько дней вперед и последнее, что мы сделаем, это напечатаем его. Причем напечатаем красиво и будем для этого использовать модуль стандартной библиотеки `pprint` - это `pretty print`. Чтобы это заработало, нам конечно же нужно импортировать модуль `pretty print`. Теперь у нас есть код, который мы должны реализовать."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "791cc1a5",
   "metadata": {},
   "outputs": [],
   "source": [
    "import pprint\n",
    "\n",
    "def _main():\n",
    "    city = CityInfo(sys.argv[1])\n",
    "    forecast = city.weather_forecast()\n",
    "    pprint.pprint(forecast)\n",
    "    \n",
    "if __name__ == \"__main__\":\n",
    "    _main()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "22e87f7b",
   "metadata": {},
   "source": [
    "`Flake8` подчеркивает нам то, что у нас ещё нет класса `CityInfo`. Давайте его создадим. `Class CityInfo`. У него будет переопределен метод `__init__`, то есть иницализатор класса и он будет принимать название города. Устанавливаем атрибут экземпляра. И также у этого класса должен быть метод `weather_forecast`, который должен возвращать прогноз погоды. Пока он ничего не будет делать."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "da8d8628",
   "metadata": {},
   "outputs": [],
   "source": [
    "class CityInfo:\n",
    "    def __init__(self, city):\n",
    "        self.city = city\n",
    "        \n",
    "    def weather_forecast(self):\n",
    "        pass"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "96ccaa75",
   "metadata": {},
   "source": [
    "Давайте подумаем о том, откуда нам брать прогноз погоды. В интернете есть замечательный ресурс `yahoo weather api`, представляемый компанией `Yahoo`. Мы будем пользоваться им. Перейдем на их сайт и посмотрим как нам получить прогноз погоды. Вот в этой формочке мы можем поэкспериментировать с их `api`. На самом деле тут уже практически готов правильный запрос. Нужно только в одном месте поменять название места, где мы хотим смотреть прогноз погоды, на наш и указать, что результат мы хотим получать не в Фаренгейтах, а в Цельсиях, температуру. Я заранее посмотрел, как это делается, конечно же и мы можем нажать кнопочку тест и `yahoo` предоставляет нам ссылку, по которой мы можем перейти и увидеть прогноз погоды в Москве. На самом деле это большой `json`, внутри которого есть много вложенных полей. И прогноз погоды скрыт достаточно глубоко. Вот он. Мы видим, что здесь несколько объектов внутри списка по дням. Здесь есть, например, сегодняшняя дата и так далее.\n",
    "\n",
    "Наша цель - получить эти данные. Однако мы не будем просто так писать код, делающий http-запрос внутри метода `weather_forecast`. Мы создадим специальный класс, который будет называтся `YahooWeatherForecast`. У него будет метод `get`, который будет принимать город. И вот именно этот класс будет ответственный за то, чтобы ходить по сети и делать запросы к `yahoo`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "31b8e09c",
   "metadata": {},
   "outputs": [],
   "source": [
    "class YahooWeatherForecast:\n",
    "    def get(self, city):\n",
    "        pass"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2cb6eb49",
   "metadata": {},
   "source": [
    "На самом деле чем это хорошо? А тем что мы этот класс впоследствии сможем подменить другим. Если вдруг с `API Yahoo` что-то случится, у нас всегда будет возможность поменять только один компонент нашей программы, только один класс заменить и весь остальной код программы продолжит работать. Давайте сделаем http-запрос. У нас в буфере обмена содержится тот url-адрес, который `Yahoo` нам предоставил. Это достаточно длинная строка и нам нужно в ней заменить название города `moscow` на то что мы получаем в аргументах, на наш `city`. По-хорошему эту строку желательно разбить на несколько, чтобы `pep8 linter` не ругался, но так как это пример, мы этого сейчас делать не будем, но на практике это нужно сделать.\n",
    "\n",
    "Теперь мы можем воспользоватся библиотечкой `requests`. Давайте сначала её импортируем. И мы можем получить данные. Делаем `requests.get(url).json` То есть преобразовываем ответ `json`'а в Python'овское представление. В данном случае это будет словарь. \n",
    "\n",
    "Теперь нам нужно достучаться до того вложенного объекта, который был в этих данных. Давайте это сделаем. Напишем, зададим переменную `forecast_data`, которая будет равнятся следующему."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "f8bf861e",
   "metadata": {},
   "outputs": [],
   "source": [
    "class YahooWeatherForecast:\n",
    "    def get(self, city):\n",
    "        url = f\"http://query.yahooapis.com/city/{city}\"\n",
    "        data = requests.get(url).json()\n",
    "        \n",
    "        forecast_data = data[\"query\"][\"forecast\"]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2dce3928",
   "metadata": {},
   "source": [
    "Смотрите, у нас внутри этих данных на верхнем уровне находится словарь `query`. Обращаемся к нему. Дальше внутри `query` у нас список `forecast`. Список с прогнозом погоды на несколько дней. Здесь дата представлена как строка и также есть самая максимальная температура за день, она доступна по ключу `high`, мы будем брать дату и вот эту температуру самую максимальную. \n",
    "\n",
    "Определим список словарей `forecast` и заполним его данными. Для этого мы проитерируемся по `forecast_data`, полученным с Yahoo и заполним `forecast` данными, пригодными для нас.\n",
    "\n",
    "Возьмем дату оттуда, а также возьмем оттуда самую максимальную температуру за день. Что еще хотелось бы здесь сразу сделать, мы видим, что дата здесь представлена как строка. Как раз для этого нам пригодится модуль `dateutil`, который мы устанавливали в самом начале, в нем содержится полезная функция `parse`, внутри модуля `dateutil.parser`, внутри пакета и мы можем ее импортировать и с помощью ее преобразовать строку, содержащую дату в объект даты из модуля `datetime` стандартной библиотеки Python. И в принципе все, мы можем сделать `return forecast` и сохранить."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "5e39fe6e",
   "metadata": {},
   "outputs": [],
   "source": [
    "from dateutil.parser import parse\n",
    "\n",
    "class YahooWeatherForecast:\n",
    "    def get(self, city):\n",
    "        url = f\"http://query.yahooapis.com/city/{city}\"\n",
    "        data = requests.get(url).json()\n",
    "        \n",
    "        forecast = []\n",
    "        forecast_data = data[\"query\"][\"forecast\"]\n",
    "        \n",
    "        for day_data in forecast_data:\n",
    "            forecast.append({\n",
    "                \"date\": parse(day_data[\"date\"]),\n",
    "                \"high_temp\": int(day_data[\"high\"])\n",
    "            })\n",
    "\n",
    "        return forecast"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8cfbb47a",
   "metadata": {},
   "source": [
    "Теперь мы вернемся к методу экземпляра `weather_forecast` и в этом методе мы обратимся к `YahooWeatherForecast` классу и вызовем его метод `get` для нужного города. Давайте сделаем, расширим метод `__init__`. Он ну нас будет принимать дополнительный параметр необязательный `forecast_provider` и ставить приватную переменную `_forecast_provider` экземпляру класса. Если же мы `forecast_provider` не передали, будет по умолчанию использоваться `YahooWeatherForecast` экземпляр."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "5b81e29b",
   "metadata": {},
   "outputs": [],
   "source": [
    "class CityInfo:\n",
    "    def __init__(self, city, forecast_provider=None):\n",
    "        self.city = city.lower()\n",
    "        self._forecast_provider = forecast_provider or YahooWeatherForecast()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "82101659",
   "metadata": {},
   "source": [
    "Теперь мы внутри метода `weather_forecast` можем вызвать `self._forecast_provider`, то есть обратиться к приватному атрибуту и вызвать у него метод `get`, передав ему `self.city`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "db98eeda",
   "metadata": {},
   "outputs": [],
   "source": [
    "class CityInfo:\n",
    "    def __init__(self, city, forecast_provider=None):\n",
    "        self.city = city.lower()\n",
    "        self._forecast_provider = forecast_provider or YahooWeatherForecast()\n",
    "        \n",
    "    def weather_forecast(self):\n",
    "        return self._forecast_provider.get(self.city)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "08480c53",
   "metadata": {},
   "source": [
    "На этом по идее у нас программа должна начать работать, давайте попробуем запустить ее. Перейдем в консоль. Мы видим, что мы получили прогноз погоды в Москве. Как мы можем улучшить наш код. Он уже сейчас работает, он достаточно расширяемый, но мы можем сделать еще лучше."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6d200fe3",
   "metadata": {},
   "source": [
    "Давайте представим, что нашим приложением, которое мы пишем, пользуется достаточно много пользователей, и на самом деле приходит множество запросов о том, чтобы посмотреть прогноз погоды в Москве. Давайте это сэмулируем. Например, пусть пришло 5 запросов. В этот момент, на каждый из 5 запросов мы будем отсылать `http` запрос к `Yahoo`, для того, чтобы получить прогноз. Это не очень хорошо, потому что каждый `http` запрос - достаточно долгая операция, давайте посмотрим, как мы можем сделать лучше.\n",
    "\n",
    "Мы можем расширить класс `YahooWeatherForecast` и добавить к нему метод `__init__`. И создать внутри экземпляра приватных переменных, которые будут содержать в себе кэш данных по городам. \n",
    "\n",
    "Кэш представляет собой словарь, который содержит прогноз погоды в определенном городе, каждый раз, когда будет вызываться метод `get` нашего класса `YahooWeatherForecast`, мы будем проверять, что если город находится в словаре, то мы будем возвращать данные, которые принадлежат этому городу из этого `city` кэша. И каждый раз, когда мы получили данные, мы будем обновлять приватный словарик, приватный атрибут и ставить городу определенный прогноз погоды."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "472c7365",
   "metadata": {},
   "outputs": [],
   "source": [
    "class YahooWeatherForecast:\n",
    "    def __init__(self):\n",
    "        self._city_cache = {}\n",
    "    \n",
    "    def get(self, city):\n",
    "        if city in self._cached_data:\n",
    "            return self._cached_data[city]\n",
    "        \n",
    "        url = f\"http://query.yahooapis.com/city/{city}\"\n",
    "        \n",
    "        print(\"sending http request\")\n",
    "        data = requests.get(url).json()\n",
    "        \n",
    "        forecast = []\n",
    "        forecast_data = data[\"query\"][\"forecast\"]\n",
    "        \n",
    "        for day_data in forecast_data:\n",
    "            forecast.append({\n",
    "                \"date\": parse(day_data[\"date\"]),\n",
    "                \"high_temp\": int(day_data[\"high\"])\n",
    "            })\n",
    "\n",
    "        return forecast"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "55a4c91c",
   "metadata": {},
   "source": [
    "Теперь, перед тем, как отправить `http` запрос, давайте напишем `print(\"sending http request\")`."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5e6c0394",
   "metadata": {},
   "source": [
    "`sending http request` напечаталось только один раз. Это во-первых и быстрее работает в итоге и мы экономим на `http` запросах, например, это может быть полезно так как api-шки сторонние часто бывают платными.\n",
    "\n",
    "В данном примере мы применили композицию классов, то есть внутри класса `CityInfo` у нас атрибуту присвоен другой класс, часто это очень хороший подход к написанию кода. В данном примере мы не обрабатывали исключения, какие здесь они могли быть. Во-первых сайт `Yahoo` мог быть недоступен, во-вторых, каждый раз, когда мы обращались к ключу словаря мог произойти `key error`, то есть ключа такого могло не произойти, потому что `Yahoo` вернул нам невалидные данные. Также когда мы преобразовывали дату из строки в объект `datetime`, также мог произойти `exception`. Вы пока обрабатывать `exception` не умеете, но вам осталось узнать о классах совсем немножко про наследование и после этого мы научим вас обрабатывать исключения в программах. В данном случае мы написали класс, который умеет получать информацию о городе, причем этот класс достаточно поддерживаемый и его компоненты легко заменяемы. И также он расширяемый."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "id": "f6663800",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "2021-01-06 00:00:00\n",
      "12\n",
      "2021-02-06 00:00:00\n",
      "18\n",
      "2021-03-06 00:00:00\n",
      "12\n",
      "2021-04-06 00:00:00\n",
      "17\n",
      "2021-05-06 00:00:00\n",
      "14\n",
      "2021-06-06 00:00:00\n",
      "14\n",
      "2021-07-06 00:00:00\n",
      "14\n",
      "2021-08-06 00:00:00\n",
      "13\n",
      "2021-09-06 00:00:00\n",
      "18\n"
     ]
    }
   ],
   "source": [
    "from random import randint\n",
    "from dateutil.parser import parse\n",
    "\n",
    "\n",
    "forecast_data = [\n",
    "    {\n",
    "        \"date\": f\"0{i}/06/2021\",\n",
    "        \"high\": randint(12, 18)\n",
    "    } for i in range(1, 10)\n",
    "]\n",
    "\n",
    "\n",
    "for day_data in forecast_data:\n",
    "    print(parse(day_data[\"date\"]))\n",
    "    print(int(day_data[\"high\"]))"
   ]
  }
 ],
 "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
}
