You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

525 lines
22 KiB
Plaintext

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

{
"cells": [
{
"cell_type": "markdown",
"id": "59ecb880",
"metadata": {},
"source": [
"# Композиция классов #"
]
},
{
"cell_type": "markdown",
"id": "c0953a7c",
"metadata": {},
"source": [
"В предыдущей лекции мы рассмотрели, как работает наследование и множественное наследование в классах Python. Как я уже говорил, если создать достаточно большую иерархию классов и использовать множественное наследование, а также большое количество классов-примесей, то итоговый код может показаться очень сложным, и его будет очень трудно читать программисту и разбираться, как он работает, что делает его менее выразительным.\n",
"\n",
"В Python существует альтернативный подход наследованию — это композиция.\n",
"\n",
"В этой лекции мы с вами на примере разберем, как работает композиция, и вы сможете сравнить, какой из подходов вам больше нравится — наследование или композиция.\n",
"\n",
"Давайте немного вспомним классы. У нас был класс \"питомец\", мы от него унаследовали класс \"собачку\" (класс `Dog`). Затем мы захотели, чтобы наши объекты классов \"собака\" могли выполнять экспорт данных, и мы ввели класс-примесь `ExportJSON`. После этого наш финальный класс `ExDog` использовал множественное наследование и наследовался от класса \"собачка\" и `ExportJSON`, тем самым он решал все наши задачи."
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "7abf9ef1",
"metadata": {},
"outputs": [],
"source": [
"class Pet:\n",
" pass\n",
"\n",
"class Dog(Pet):\n",
" pass\n",
" \n",
"class ExportJSON:\n",
" pass\n",
"\n",
"class ExDog(Dog, ExportJSON):\n",
" pass"
]
},
{
"cell_type": "markdown",
"id": "609625a7",
"metadata": {},
"source": [
"Давайте представим, что нам понадобится экспортировать данные не только в формате `json`, но и еще в другом формате, например, пусть будет это `XML`. Как тогда изменится структура нашей программы? Тогда, очевидно, нам нужно будет добавить еще один класс-примесь, и тогда наш код будет выглядеть так, как показано на слайде, то есть пока не вдаемся в подробности реализации самих классов-примесей."
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "e3465f4b",
"metadata": {},
"outputs": [],
"source": [
"class ExportJSON:\n",
" def to_json(self):\n",
" pass\n",
"\n",
"class ExportXML:\n",
" def to_xml(self):\n",
" pass\n",
" \n",
"class ExDog(Dog, ExportJSON, ExportXML):\n",
" pass"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "6e5b3966",
"metadata": {},
"outputs": [],
"source": [
"dog = ExDog(\"Фокс\", \"мопс\")\n",
"dog.to_xml()\n",
"dog.to_json()"
]
},
{
"cell_type": "markdown",
"id": "55fa6dc7",
"metadata": {},
"source": [
"У нас появился наш новый класс-примесь `ExportXML`, у него есть свой собственный метод `to_xml`, и наш итоговый класс `ExDog` теперь наследуется уже от трех классов-родителей. Тем самым объекты этого класса `ExDog` смогут экспортировать данные как в `json`, так и в `xml`.\n",
"\n",
"Давайте представим, что нам нужно будет добавлять еще несколько методов для экспорта данных. Какие сложности могут возникнуть в таком случае?\n",
"\n",
"Во-первых, нам постоянно придется изменять код нашего класса `ExDog`, постоянно дописывать туда новые классы-примеси.\n",
"\n",
"Во-вторых, это уже сильно усложнит сам код, и в итоговой программе нам нужно будет вызывать разные методы этих классов-примесей, то есть мы будем постоянно что-то изменять в своей программе. И это уже не то, чего хотелось бы нам достичь.\n",
"\n",
"Давайте попробуем рассмотреть, как в таком случае работает композиция. Для этого нам понадобится Jupyter Notebook. Давайте попробуем решить эту задачу немного другим способом."
]
},
{
"cell_type": "markdown",
"id": "f698b300",
"metadata": {},
"source": [
"Предположим, у нас будет класс для экспорта. Давайте объявим его. Пусть он будет называться `PetExport` и у него будет один метод, `export`. Обратите внимание, я сейчас использовал генерацию исключения. Об исключениях мы с вами будем говорить в последующих лекциях, а пока вам нужно для себя усвоить, что мы не будем создавать объекты данного класса `PetExport` и он предназначен только для наследования. Давайте объявим другие классы, которые будут заниматься экспортом данных. `JSON`. Также наш класс `export` должен принимать сам объект, который он будет экспортировать. Пока опустим реализацию самого экспорта. Также добавим класс для экспорта данных в формате `xml`."
]
},
{
"cell_type": "code",
"execution_count": 27,
"id": "b2e18c93",
"metadata": {},
"outputs": [],
"source": [
"class PetExport:\n",
" def export(self):\n",
" raise NotImplementedError\n",
"\n",
"class ExportJSON:\n",
" def export(self, dog):\n",
" pass\n",
" \n",
"class ExportXML:\n",
" def export(self, dog):\n",
" pass"
]
},
{
"cell_type": "markdown",
"id": "b5b69659",
"metadata": {},
"source": [
"Итак, у нас готова иерархия классов для экспорта данных. Давайте теперь вспомним наши классы. \"Питомец\", у которого у каждого питомца было имя, мы его сохраняли в атрибуте `name`, и также класс \"собачка\", у которого появился дополнительный атрибут порода, `breed`, и мы сохранили его в одноименном атрибуте."
]
},
{
"cell_type": "code",
"execution_count": 28,
"id": "14a2f4cc",
"metadata": {},
"outputs": [],
"source": [
"class Pet:\n",
" def __init__(self, name=None):\n",
" self.name = name\n",
" \n",
" \n",
"class Dog(Pet):\n",
" def __init__(self, name, breed=None):\n",
" super().__init__(name)\n",
" self.breed = breed"
]
},
{
"cell_type": "markdown",
"id": "b3b38ead",
"metadata": {},
"source": [
"Давайте теперь объявим класс `ExDog`. Теперь мы не будем использовать свое множественное наследование, мы будем расширять существующий класс `Dog`. Переопределим инициализатор. Он тоже будет принимать на вход имя питомца, породу, а также дополнительный объект для экспорта данных. Вызовем конструктор базового класса и сохраним объект `exporter` в `self`."
]
},
{
"cell_type": "code",
"execution_count": 29,
"id": "3098e00d",
"metadata": {},
"outputs": [],
"source": [
"class ExDog(Dog):\n",
" def __init__(self, name, breed=None, exporter=None):\n",
" super().__init__(name, breed)\n",
" self._exporter = exporter"
]
},
{
"cell_type": "markdown",
"id": "40256353",
"metadata": {},
"source": [
"Обратите внимание, что в данном классе мы не используем наследование. Вместо этого мы используем композицию и передаем нужный объект для экспорта в инициализаторе этого класса. Давайте добавим ему метод `export`, и теперь наш класс сможет экспортировать данные."
]
},
{
"cell_type": "code",
"execution_count": 30,
"id": "e3cd85ac",
"metadata": {},
"outputs": [],
"source": [
"class ExDog(Dog):\n",
" def __init__(self, name, breed=None, exporter=None):\n",
" super().__init__(name, breed)\n",
" self._exporter = exporter\n",
" \n",
" def export(self):\n",
" return self._exporter.export(self)"
]
},
{
"cell_type": "markdown",
"id": "5484df82",
"metadata": {},
"source": [
"Делать он это будет при помощи нашего `exporter`'а. Передадим ему `self` для экспорта. \n",
"\n",
"Давайте попробуем создать экземпляр нашего класса `ExDog`. Пусть это будет собачка \"Шарик\", порода \"Дворняга\". Предположим, мы хотим, чтобы объект этого класса умел экспортировать свои данные в `xml`. Давайте передадим нужный `exporter`."
]
},
{
"cell_type": "code",
"execution_count": 31,
"id": "0a9757ad",
"metadata": {},
"outputs": [],
"source": [
"dog = ExDog(\"Шарик\", \"Дворняга\", exporter=ExportXML())"
]
},
{
"cell_type": "markdown",
"id": "63846315",
"metadata": {},
"source": [
"Обратите внимание, что при использовании композиции нужный объект создается именно в момент выполнения уже конкретной программы. Давайте попробуем выполнить метод `export`."
]
},
{
"cell_type": "code",
"execution_count": 32,
"id": "d2a062c4",
"metadata": {},
"outputs": [],
"source": [
"dog.export()"
]
},
{
"cell_type": "markdown",
"id": "f88aac87",
"metadata": {},
"source": [
"Осталось реализовать только методы для экспорта в начальной иерархии классов. Давайте сделаем это. С `json` все просто, мы уже разбирали пример, используя модуль `json` и метод `dumps`."
]
},
{
"cell_type": "code",
"execution_count": 33,
"id": "cfded06f",
"metadata": {},
"outputs": [],
"source": [
"import json\n",
"\n",
"class ExportJSON:\n",
" def export(self, dog):\n",
" return json.dumps(\n",
" {\n",
" \"name\": dog.name,\n",
" \"breed\": dog.breed\n",
" }\n",
" )"
]
},
{
"cell_type": "markdown",
"id": "09aad031",
"metadata": {},
"source": [
"Давайте реализуем теперь метод `export` в классе `ExportXML`. Реализация может выглядеть достаточно просто. Создаем сам `xml`. В нем будут объекты `name` и порода нашей собаки. Всё, осталось отформатировать данную строку при помощи метода `format`."
]
},
{
"cell_type": "code",
"execution_count": 34,
"id": "376dfd98",
"metadata": {},
"outputs": [],
"source": [
"class ExportXML:\n",
" def export(self, dog):\n",
" return \"\"\"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n",
"<dog>\n",
" <name>{name}</name>\n",
" <breed>{breed}</breed>\n",
"</dog>\n",
"\"\"\".format(name=dog.name, breed=dog.breed)"
]
},
{
"cell_type": "code",
"execution_count": 35,
"id": "bcfad312",
"metadata": {
"scrolled": true
},
"outputs": [
{
"data": {
"text/plain": [
"'<?xml version=\"1.0\" encoding=\"utf-8\"?>\\n<dog>\\n <name>Шарик</name>\\n <breed>Дворняга</breed>\\n</dog>\\n'"
]
},
"execution_count": 35,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"dog = ExDog(\"Шарик\", \"Дворняга\", exporter=ExportXML())\n",
"dog.export()"
]
},
{
"cell_type": "markdown",
"id": "5be78f28",
"metadata": {},
"source": [
"Точно так же мы с легкостью можем сделать экспорт данных и в формате `json`. Можем создать другую собачку. Пусть это будет \"Тузик\" другой породы."
]
},
{
"cell_type": "code",
"execution_count": 39,
"id": "ef8c4c01",
"metadata": {
"scrolled": true
},
"outputs": [
{
"data": {
"text/plain": [
"'{\"name\": \"\\\\u0422\\\\u0443\\\\u0437\\\\u0438\\\\u043a\", \"breed\": \"\\\\u041c\\\\u043e\\\\u043f\\\\u0441\"}'"
]
},
"execution_count": 39,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"dog = ExDog(\"Тузик\", \"Мопс\", exporter=ExportJSON())\n",
"dog.export()"
]
},
{
"cell_type": "markdown",
"id": "c52e1bfa",
"metadata": {},
"source": [
"Однако, неудобно, если каждый раз задавать метод для экспорта. Давайте немного изменим наш класс `ExDog` и зададим метод для экспорта по умолчанию. Можно это сделать следующим образом. \n",
"\n",
"Давайте выполним еще одну важную вещь. Мы проверим на то, является ли переданный объект экземпляром класса `PetExport` и вообще может ли он выполнять экспорт данных. Для этого мы можем воспользоваться проверкой `isinstance`. Нам нужен наш `exporter` и указываем класс. Что делать, если нам передали объект, который не может выполнять экспорт? Давайте пока сгенерируем исключение.\n",
"Для вас это будет означать, что программа дальше не сможет продолжить свою работу и будет остановлена. Например, `ValueError` нам подойдет."
]
},
{
"cell_type": "code",
"execution_count": 41,
"id": "624c763b",
"metadata": {},
"outputs": [],
"source": [
"class ExDog(Dog):\n",
" def __init__(self, name, breed=None, exporter=None):\n",
" super().__init__(name, breed)\n",
" \n",
" self._exporter = exporter or ExportJSON()\n",
" \n",
" if not isinstance(self._exporter, PetExport):\n",
" raise ValueError(\"bad exporter\", exporter)\n",
" \n",
" def export(self):\n",
" return self._exporter.export(self)"
]
},
{
"cell_type": "code",
"execution_count": 42,
"id": "812aff80",
"metadata": {},
"outputs": [],
"source": [
"class ExportJSON(PetExport):\n",
" def export(self, dog):\n",
" return json.dumps(\n",
" {\n",
" \"name\": dog.name,\n",
" \"breed\": dog.breed\n",
" }\n",
" )\n",
"\n",
"class ExportXML(PetExport):\n",
" def export(self, dog):\n",
" return \"\"\"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n",
"<dog>\n",
" <name>{name}</name>\n",
" <breed>{breed}</breed>\n",
"</dog>\n",
"\"\"\".format(name=dog.name, breed=dog.breed)"
]
},
{
"cell_type": "code",
"execution_count": 43,
"id": "41174209",
"metadata": {
"scrolled": true
},
"outputs": [
{
"data": {
"text/plain": [
"'{\"name\": \"\\\\u0422\\\\u0443\\\\u0437\\\\u0438\\\\u043a\", \"breed\": \"\\\\u041c\\\\u043e\\\\u043f\\\\u0441\"}'"
]
},
"execution_count": 43,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"dog = ExDog(\"Тузик\", \"Мопс\")\n",
"dog.export()"
]
},
{
"cell_type": "markdown",
"id": "fe1efbd1",
"metadata": {},
"source": [
"Теперь, если мы не объявим объект `exporter`, по умолчанию наша собачка будет экспортировать данные в `json`. Давайте еще раз посмотрим на то, какой код у нас получился, и подумаем, что с ним произойдет, если его нужно будет расширить. "
]
},
{
"cell_type": "code",
"execution_count": 44,
"id": "4bb4fda8",
"metadata": {},
"outputs": [],
"source": [
"import json\n",
"\n",
"class Pet:\n",
" def __init__(self, name=None):\n",
" self.name = name\n",
" \n",
" \n",
"class Dog(Pet):\n",
" def __init__(self, name, breed=None):\n",
" super().__init__(name)\n",
" self.breed = breed\n",
" \n",
" \n",
"class PetExport:\n",
" def export(self):\n",
" raise NotImplementedError\n",
"\n",
" \n",
"class ExportJSON(PetExport):\n",
" def export(self, dog):\n",
" return json.dumps(\n",
" {\n",
" \"name\": dog.name,\n",
" \"breed\": dog.breed\n",
" }\n",
" )\n",
"\n",
" \n",
"class ExportXML(PetExport):\n",
" def export(self, dog):\n",
" return \"\"\"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n",
"<dog>\n",
" <name>{name}</name>\n",
" <breed>{breed}</breed>\n",
"</dog>\n",
"\"\"\".format(name=dog.name, breed=dog.breed)\n",
" \n",
" \n",
"class ExDog(Dog):\n",
" def __init__(self, name, breed=None, exporter=None):\n",
" super().__init__(name, breed)\n",
" \n",
" self._exporter = exporter or ExportJSON()\n",
" \n",
" if not isinstance(self._exporter, PetExport):\n",
" raise ValueError(\"bad exporter\", exporter)\n",
" \n",
" def export(self):\n",
" return self._exporter.export(self)"
]
},
{
"cell_type": "markdown",
"id": "291bee59",
"metadata": {},
"source": [
"Предположим, если нам нужно будет добавить в этот код новый метод для экспорта, мы с легкостью сможем сделать это. Просто объявим новый класс, добавим его в существующую иерархию для экспорта, а класс `ExDog` теперь мы больше менять не будем. Таким образом, если у нас система будет усложняться, наш класс `ExDog` будет оставаться неизменяемым или неизменным. А экспортировать в различные форматы мы уже сможем легко и удобно в итоговой программе, подставив нужный exporter или создав его.\n",
"\n",
"Не пугайтесь, если вам сейчас непонятны плюсы данного подхода по отношению к наследованию. Чем вы больше будете программировать на Python, пробовать и реализовывать различные подходы, тем вам станет понятно, где действительно лучше применять наследование, а в каких местах следует использовать композицию."
]
}
],
"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
}