{
"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 \"\"\"\n",
"\n",
" {name}\n",
" {breed}\n",
"\n",
"\"\"\".format(name=dog.name, breed=dog.breed)"
]
},
{
"cell_type": "code",
"execution_count": 35,
"id": "bcfad312",
"metadata": {
"scrolled": true
},
"outputs": [
{
"data": {
"text/plain": [
"'\\n\\n Шарик\\n Дворняга\\n\\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 \"\"\"\n",
"\n",
" {name}\n",
" {breed}\n",
"\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 \"\"\"\n",
"\n",
" {name}\n",
" {breed}\n",
"\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
}