{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Python\n", "\n", "## Работа с файлами\n", "\n", "### Раздел 1. Общие сведения\n", "\n", "В некоторых проектах просто невозможно избежать необходимости создавать хранилище данных. В случаях, когда речь идет о большом количестве данных, которые реализуют какую-то схему (в контексте релляционных БД это озночает, что они могут быть описаны в виде таблицы или совокупности таблиц), лучше всего подходят базы данных. Но очевидно, что создавать базу данных только лишь для хранения конфигурации — не самое оптимальное решение. К тому же, часть задач требует чтения информации из файлов или записи информации в файлы. Таким образом, понимание работы с файлами необходима и может пригодиться в очень задачах из различных областей.\n", "\n", "### Раздел 2. Взаимодействие с binary-файлами\n", "\n", "Прежде всего необходимо отметить, что в Python есть два типа данных, которые позволяют оперировать над двоичными данными: bytes и bytearray. Оба из них содержат последовательность из нуля и более целых чисел от 0 до 255 включительно. Оба типа похожи на тип string и предоставляют схожий интерфейс. В дополнение, bytes является неизменяемым, а bytearray — изменяемым. В результате, для последнего доступен интерфейс, схожий со списками.\n", "\n", "Взаимодействие с двоичным представлением данных как правило позволяет добиться наибольшей компрессии данных. Самым удобным в данном случае является использование модуля Pickles, хотя ручная обработка бинарных данных может помочь сэкономить немного дискового пространства.\n", "\n", "#### Pickles с опциональным сжатием\n", "\n", "Pickles — самый удобный способ сохранять в и загружать из файлов данные, представленные в виде объектов языка Python. При этом Pickles не предоставляет никаких механизмов защиты, так что загрузка объекта, полученного извне, несет с собой потенциальные угрозы.\n", "\n", "Вот пример кода для экспорт объекта при помощи Pickles (из Саммерфилда):" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def export_pickle(self, filename, compress=False):\n", " fh = None\n", " try:\n", " if compress:\n", " fh = gzip.open(filename, \"wb\")\n", " else:\n", " fh = open(filename, \"wb\")\n", " pickle.dump(self, fh, pickle.HIGHEST_PROTOCOL)\n", " return True\n", " except (EnvironmentError, pickle.PicklingError) as err:\n", " print(\"{0}: export error: {1}\".format(os.path.basename(sys.argv[0]), err))\n", " return False\n", " finally:\n", " if fh is not None:\n", " fh.close()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Если был передан параметр compression со значением True, то будет выполнено сжатие при помощи модуля gzip. В противном случае, дескриптор файла будет получен при помощи функции open. И в том, и в другом случае доступ к файлу будет осуществляться в бинарном режиме для чтения (wb). Для записи объекта в файл используется вызов dump модуля pickle, с параметром pickle.HIGHEST_PROTOCOL который отвечает за использование компактного двоичного формата.\n", "\n", "В случае, если мы получили какое-то исключение, мы его обработает, выведя сообщение об ошибке и вернув False в качестве результата.\n", "\n", "Если никаких исключений не было, то возвращается значение True.\n", "\n", "В любом случае, дескриптор файла необходимо закрыть, так что в блоке finally содержится соответствующий вызов, если дескриптор файла не является None на момент вызова." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "##### Небольшая вставка про магические числа\n", "\n", "Магические числа (magic numbers) — специальные последовательности бит, которые располагаются где-то вначале файла, которые позволяют определить, к какому типу относится данный файл. За всеми подробностями обращайтесь к `man file` и `man magic`, но если коротко, то волшебные числа определяются при помощи смещения, типа значения и самого значения. Т.е. вы можете определить собственное магическое число, которое представлено, скажем строкой \"Lambda\" со смещением 42. Тогда взяв любой файл, вы сможете определить, относится он или не относится к определенному вами типу." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Перейдем теперь к импорту файлов при помощи Pickles (код также из Саммерфилда):" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "GZIP_MAGIC = b\"\\x1F\\x8B\"\n", "\n", "def import_pickle(self, filename):\n", " fh = None\n", " try:\n", " fh = open(filename, \"rb\")\n", " magic = fh.read(len(GZIP_MAGIC))\n", " if magic == GZIP_MAGIC:\n", " fh.close()\n", " fh = gzip.open(filename, \"rb\")\n", " else:\n", " fh.seek(0)\n", " self.clear()\n", " self.update(pickle.load(fh))\n", " return True\n", " except (EnvironmentError, pickle.UnpicklingError) as err:\n", " print(\"{0}: import error: {1}\".format(os.path.basename(sys.argv[0]), err))\n", " return False\n", " finally:\n", " if fh is not None:\n", " fh.close()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Здесь мы просто открываем файл, проверяем, есть ли в начале него магическое число, отвечающее формату gzip. Если есть, то закрываем дескриптор файла и открываем снова при помощи вызова open модуля gzip. В противном случае возвращаемся к началу файла при помощи метода seek файлового дескриптора. В обоих случаях открытие происходит с двоичным доступом для чтения (rb). После чего очищаем объект self при помощи метода clear и заполняем его при помощи метода update результатом работы вызова load модуля pickle, которому передаем файловый дескриптор.\n", "\n", "Если в результате было получено исключение, выведем его и вернем False. В противном случае возвращаемое значение равно True. В блоке finally закрываем дескриптор файла, если он существует." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Раздел 3. Взаимодействие с тексстовыми файлами\n", "\n", "Запись текстовых файлов — очень простой процесс, но вот парсинг данных из текстового файла — занятие трудозатратное.\n", "\n", "#### Запись в текстовые файлы\n", "\n", "Мы можем записать текст в виде пар key=value, обозначив данные narrative (описание произошедшего) при помощи маркеров .NARRATIVE_START и .NARRATIVE_END\n", "\n", "Например:" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "[20070927022009C]\n", "\n", "date=2007-09-27\n", "\n", "aircraft_id=1675B\n", "\n", "aircraft_type=DHC-2-MK1\n", "\n", "airport=MERLE K (MUDHOLE) SMITH\n", "\n", "pilot_percent_hours_on_type=46.1538461538\n", "\n", "pilot_total_hours=13000\n", "\n", "midair=0\n", "\n", ".NARRATIVE_START.\n", "\n", " ACCORDING TO THE PILOT, THE DRAG LINK FAILED DUE TO AN OVERSIZED\n", " TAIL WHEEL TIRE LANDING ON HARD SURFACE.\n", " \n", ".NARRATIVE_END." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Запись текста в файл будет производиться следующим образом (код из Саммерфилда):" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def export_text(self, filename):\n", " wrapper = textwrap.TextWrapper(initial_indent=\" \", subsequent_indent=\" \")\n", " fh = None\n", " try:\n", " fh = open(filename, \"w\", encoding=\"utf8\")\n", " for incident in self.values():\n", " narrative = \"\\n\".join(wrapper.wrap(incident.narrative.strip()))\n", " fh.write(\"[{0.report_id}]\\n\"\n", " \"date={0.date!s}\\n\"\n", " \"aircraft_id={0.aircraft_id}\\n\"\n", " \"aircraft_type={0.aircraft_type}\\n\"\n", " \"airport={airport}\\n\"\n", " \"pilot_percent_hours_on_type=\"\n", " \"{0.pilot_percent_hours_on_type}\\n\"\n", " \"pilot_total_hours={0.pilot_total_hours}\\n\"\n", " \"midair={0.midair:d}\\n\"\n", " \".NARRATIVE_START.\\n{narrative}\\n\"\n", " \".NARRATIVE_END.\\n\\n\".format(incident, airport=incident.airport.strip(), narrative=narrative))\n", " if fh is not None:\n", " fh.close()\n", " return True" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Здесь мы просто открываем файл для записи в текстовом режиме (w) в кодировке utf-8 и делаем форматный вывод нашего объекта." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Чтение текстового файла\n", "\n", "Чтение текстового файла может производиться при помощи \"ручной\" обработки текста:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def import_text_manual(self, filename):\n", " fh = None\n", " try:\n", " fh = open(filename, encoding=\"utf8\")\n", " self.clear()\n", " data = {}\n", " narrative = None\n", " for lino, line in enumerate(fh, start=1):\n", " line = line.rstrip()\n", " if not line and narrative is None:\n", " continue\n", " if narrative is not None:\n", " if line == \".NARRATIVE_END.\":\n", " data[\"narrative\"] = textwrap.dedent(\n", " narrative).strip()\n", " if len(data) != 9:\n", " raise IncidentError(\"missing data on \"\n", " \"line {0}\".format(lino))\n", " incident = Incident(**data)\n", " self[incident.report_id] = incident\n", " data = {}\n", " narrative = None\n", " else:\n", " narrative += line + \"\\n\"\n", " elif (not data and line[0] == \"[\" and line[-1] == \"]\"):\n", " data[\"report_id\"] = line[1:-1]\n", " elif \"=\" in line:\n", " key, value = line.split(\"=\", 1)\n", " if key == \"date\":\n", " data[key] = datetime.datetime.strptime(value, \"%Y-%m-%d\").date()\n", " elif key == \"pilot_percent_hours_on_type\":\n", " data[key] = float(value)\n", " elif key == \"pilot_total_hours\":\n", " data[key] = int(value)\n", " elif key == \"midair\":\n", " data[key] = bool(int(value))\n", " else:\n", " data[key] = value\n", " elif line == \".NARRATIVE_START.\":\n", " narrative = \"\"\n", " else:\n", " raise KeyError(\"parsing error on line {0}\".format(\n", " lino))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Как видно, парсинг файла \"вручную\" сопряжено с огромной головной болью, необходимо вручную отлавливать ключи и т.д.\n", "\n", "С другой стороны, можно подключить инструментарий регулярных выражений для парсинга текста:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def import_text_regex(self, filename):\n", " incident_re = re.compile(\n", " r\"\\[(?P<id>[^]]+)\\](?P<keyvalues>.+?)\"\n", " r\"^\\.NARRATIVE_START\\.$(?P<narrative>.*?)\"\n", " r\"^\\.NARRATIVE_END\\.$\",\n", " re.DOTALL|re.MULTILINE)\n", " key_value_re = re.compile(r\"^\\s*(?P<key>[^=]+?)\\s*=\\s*\"\n", " r\"(?P<value>.+?)\\s*$\", re.MULTILINE)\n", " fh = None\n", " try:\n", " fh = open(filename, encoding=\"utf8\")\n", " self.clear()\n", " for incident_match in incident_re.finditer(fh.read()):\n", " data = {}\n", " data[\"report_id\"] = incident_match.group(\"id\")\n", " data[\"narrative\"] = textwrap.dedent(\n", " incident_match.group(\"narrative\")).strip()\n", " keyvalues = incident_match.group(\"keyvalues\")\n", " for match in key_value_re.finditer(keyvalues):\n", " data[match.group(\"key\")] = match.group(\"value\")\n", " data[\"date\"] = datetime.datetime.strptime(\n", " data[\"date\"], \"%Y-%m-%d\").date()\n", " data[\"pilot_percent_hours_on_type\"] = (\n", " float(data[\"pilot_percent_hours_on_type\"]))\n", " data[\"pilot_total_hours\"] = int(\n", " data[\"pilot_total_hours\"])\n", " data[\"midair\"] = bool(int(data[\"midair\"]))\n", " if len(data) != 9:\n", " raise IncidentError(\"missing data\")\n", " incident = Incident(**data)\n", " self[incident.report_id] = incident\n", " return True" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Регулярные выражения в значительной степени облегчают поиск паттернов (они для этого и нужны) в текстовых файлах." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Раздел 4. Работа с XML-файлами\n", "\n", "XML — специальный язык для разметки данных. Работа с ним может осуществляться при помощи модуля Element Tree, DOM и в ручном режиме, а также при помощи Simple API for XML (SAX).\n", "\n", "#### Использование Element Trees\n", "\n", "Запись XML:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def export_xml_etree(self, filename):\n", " root = xml.etree.ElementTree.Element(\"incidents\")\n", " for incident in self.values():\n", " element = xml.etree.ElementTree.Element(\"incident\",\n", " report_id=incident.report_id,\n", " date=incident.date.isoformat(),\n", " aircraft_id=incident.aircraft_id,\n", " aircraft_type=incident.aircraft_type,\n", " pilot_percent_hours_on_type=str(\n", " incident.pilot_percent_hours_on_type),\n", " pilot_total_hours=str(incident.pilot_total_hours),\n", " midair=str(int(incident.midair)))\n", " airport = xml.etree.ElementTree.SubElement(element,\n", " \"airport\")\n", " airport.text = incident.airport.strip()\n", " narrative = xml.etree.ElementTree.SubElement(element,\n", " \"narrative\")\n", " narrative.text = incident.narrative.strip()\n", " root.append(element)\n", " tree = xml.etree.ElementTree.ElementTree(root)\n", " try:\n", " tree.write(filename, \"UTF-8\")\n", " except EnvironmentError as err:\n", " print(\"{0}: import error: {1}\".format(\n", " os.path.basename(sys.argv[0]), err))\n", " return False\n", " return True" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Здесь мы просто проходим по вершинам дерева и заполняем соответствующие данные.\n", "\n", "Чтение XML:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def import_xml_etree(self, filename):\n", " try:\n", " tree = xml.etree.ElementTree.parse(filename)\n", " except (EnvironmentError,\n", " xml.parsers.expat.ExpatError) as err:\n", " print(\"{0}: import error: {1}\".format(\n", " os.path.basename(sys.argv[0]), err))\n", " return False\n", " self.clear()\n", " for element in tree.findall(\"incident\"):\n", " try:\n", " data = {}\n", " for attribute in (\"report_id\", \"date\", \"aircraft_id\",\n", " \"aircraft_type\",\n", " \"pilot_percent_hours_on_type\",\n", " \"pilot_total_hours\", \"midair\"):\n", " data[attribute] = element.get(attribute)\n", " data[\"date\"] = datetime.datetime.strptime(\n", " data[\"date\"], \"%Y-%m-%d\").date()\n", " data[\"pilot_percent_hours_on_type\"] = (\n", " float(data[\"pilot_percent_hours_on_type\"]))\n", " data[\"pilot_total_hours\"] = int(\n", " data[\"pilot_total_hours\"])\n", " data[\"midair\"] = bool(int(data[\"midair\"]))\n", " data[\"airport\"] = element.find(\"airport\").text.strip()\n", " narrative = element.find(\"narrative\").text\n", " data[\"narrative\"] = (narrative.strip() if narrative is not None else \"\")\n", " incident = Incident(**data)\n", " self[incident.report_id] = incident\n", " except (ValueError, LookupError, IncidentError) as err:\n", " print(\"{0}: import error: {1}\".format(\n", " os.path.basename(sys.argv[0]), err))\n", " return False\n", " return True" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "В данном случае логика обратная, загружаем дерево, обходим дерево и заполняем объект self данными.\n", "\n", "#### Чтение и запись XML при помощи DOM\n", "\n", "Запись:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def export_xml_dom(self, filename):\n", " dom = xml.dom.minidom.getDOMImplementation()\n", " tree = dom.createDocument(None, \"incidents\", None)\n", " root = tree.documentElement\n", " for incident in self.values():\n", " element = tree.createElement(\"incident\")\n", " for attribute, value in (\n", " (\"report_id\", incident.report_id),\n", " (\"date\", incident.date.isoformat()),\n", " (\"aircraft_id\", incident.aircraft_id),\n", " (\"aircraft_type\", incident.aircraft_type),\n", " (\"pilot_percent_hours_on_type\",\n", " str(incident.pilot_percent_hours_on_type)),\n", " (\"pilot_total_hours\",\n", " str(incident.pilot_total_hours)),\n", " (\"midair\", str(int(incident.midair)))):\n", " element.setAttribute(attribute, value)\n", " for name, text in ((\"airport\", incident.airport), (\"narrative\", incident.narrative)):\n", " text_element = tree.createTextNode(text)\n", " name_element = tree.createElement(name)\n", " name_element.appendChild(text_element)\n", " element.appendChild(name_element)\n", " root.appendChild(element)\n", " fh = None\n", " try:\n", " fh = open(filename, \"w\", encoding=\"utf8\")\n", " tree.writexml(fh, encoding=\"UTF-8\")\n", " return True" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "\n", "Чтение:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def import_xml_dom(self, filename):\n", " def get_text(node_list):\n", " text = []\n", " for node in node_list:\n", " if node.nodeType == node.TEXT_NODE:\n", " text.append(node.data)\n", " return \"\".join(text).strip()\n", " \n", " try:\n", " dom = xml.dom.minidom.parse(filename)\n", " except (EnvironmentError,\n", " xml.parsers.expat.ExpatError) as err:\n", " print(\"{0}: import error: {1}\".format(\n", " os.path.basename(sys.argv[0]), err))\n", " return False\n", " self.clear()\n", " for element in dom.getElementsByTagName(\"incident\"):\n", " try:\n", " data = {}\n", " for attribute in (\"report_id\", \"date\", \"aircraft_id\",\n", " \"aircraft_type\",\n", " \"pilot_percent_hours_on_type\",\n", " \"pilot_total_hours\", \"midair\"):\n", " data[attribute] = element.getAttribute(attribute)\n", " data[\"date\"] = datetime.datetime.strptime(\n", " data[\"date\"], \"%Y-%m-%d\").date()\n", " data[\"pilot_percent_hours_on_type\"] = (\n", " float(data[\"pilot_percent_hours_on_type\"]))\n", " data[\"pilot_total_hours\"] = int(\n", " data[\"pilot_total_hours\"])\n", " data[\"midair\"] = bool(int(data[\"midair\"]))\n", " airport = element.getElementsByTagName(\"airport\")[0]\n", " data[\"airport\"] = get_text(airport.childNodes)\n", " narrative = element.getElementsByTagName(\n", " \"narrative\")[0]\n", " data[\"narrative\"] = get_text(narrative.childNodes)\n", " incident = Incident(**data)\n", " self[incident.report_id] = incident\n", " except (ValueError, LookupError, IncidentError) as err:\n", " print(\"{0}: import error: {1}\".format(\n", " os.path.basename(sys.argv[0]), err))\n", " return False\n", " return True" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Запись XML-файлов \"вручную\"" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def export_xml_manual(self, filename):\n", " fh = None\n", " try:\n", " fh = open(filename, \"w\", encoding=\"utf8\")\n", " fh.write('<?xml version=\"1.0\" encoding=\"UTF-8\"?>\\n')\n", " fh.write(\"<incidents>\\n\")\n", " for incident in self.values():\n", " fh.write('<incident report_id={report_id} '\n", " 'date=\"{0.date!s}\" '\n", " 'aircraft_id={aircraft_id} '\n", " 'aircraft_type={aircraft_type} '\n", " 'pilot_percent_hours_on_type='\n", " '\"{0.pilot_percent_hours_on_type}\" '\n", " 'pilot_total_hours=\"{0.pilot_total_hours}\" '\n", " 'midair=\"{0.midair:d}\">\\n'\n", " '<airport>{airport}</airport>\\n'\n", " '<narrative>\\n{narrative}\\n</narrative>\\n'\n", " '</incident>\\n'.format(incident,\n", " report_id=xml.sax.saxutils.quoteattr(\n", " incident.report_id),\n", " aircraft_id=xml.sax.saxutils.quoteattr(\n", " incident.aircraft_id),\n", " aircraft_type=xml.sax.saxutils.quoteattr(\n", " incident.aircraft_type),\n", " airport=xml.sax.saxutils.escape(incident.airport),\n", " narrative=\"\\n\".join(textwrap.wrap(\n", " xml.sax.saxutils.escape(\n", " incident.narrative.strip()), 70))))\n", " fh.write(\"</incidents>\\n\")\n", " return True" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Чтение XML при помощи SAX" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def import_xml_sax(self, filename):\n", " fh = None\n", " try:\n", " handler = IncidentSaxHandler(self)\n", " parser = xml.sax.make_parser()\n", " parser.setContentHandler(handler)\n", " parser.parse(filename)\n", " return True\n", " except (EnvironmentError, ValueError, IncidentError,\n", " xml.sax.SAXParseException) as err:\n", " print(\"{0}: import error: {1}\".format(\n", " os.path.basename(sys.argv[0]), err))\n", " return False\n", "\n", "class IncidentSaxHandler(xml.sax.handler.ContentHandler):\n", " def __init__(self, incidents):\n", " super().__init__()\n", " self.__data = {}\n", " self.__text = \"\"\n", " self.__incidents = incidents\n", " self.__incidents.clear()\n", "\n", " def startElement(self, name, attributes):\n", " if name == \"incident\":\n", " self.__data = {}\n", " for key, value in attributes.items():\n", " if key == \"date\":\n", " self.__data[key] = datetime.datetime.strptime(\n", " value, \"%Y-%m-%d\").date()\n", " elif key == \"pilot_percent_hours_on_type\":\n", " self.__data[key] = float(value)\n", " elif key == \"pilot_total_hours\":\n", " self.__data[key] = int(value)\n", " elif key == \"midair\":\n", " self.__data[key] = bool(int(value))\n", " else:\n", " self.__data[key] = value\n", " self.__text = \"\"\n", "\n", " def endElement(self, name):\n", " if name == \"incident\":\n", " if len(self.__data) != 9:\n", " raise IncidentError(\"missing data\")\n", " incident = Incident(**self.__data)\n", " self.__incidents[incident.report_id] = incident\n", " elif name in frozenset({\"airport\", \"narrative\"}):\n", " self.__data[name] = self.__text.strip()\n", " self.__text = \"\"\n", "\n", " def characters(self, text):\n", " self.__text += text" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Раздел 5. Работа с файлами, формалированными в других стандартах описания данных\n", "\n", "Зачастую конфигурации удобно хранить в форматах JSON и Yaml. Доступ осуществляется соответственно при помощи модулей JSON и PyYaml.\n", "\n", "Чтение и запись из производится при помощи методов load и dump из соответствующих классов." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Раздел 6. Домашнее задание\n", "\n", "- Выполнить все упражнения после 7 главы Саммерфилда\n", "- Прочитать в Саммерфилде про ручную работу с binary-файлами и Random Access binary-файлами\n", "- Почитать про Big Endian / Little Endian\n", "- Почитать про Document Object Model (DOM)\n", "- Выполнить субботнюю задачу\n", "\n", "Для самых настойчивых:\n", "\n", "- Написать свой reader и writer для файлов JSON и/или Yaml" ] } ], "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.6" } }, "nbformat": 4, "nbformat_minor": 2 }