{ "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 }