{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Наследование\n", "\n", "Наследование является ключевым аспектом ООП и, конечно, доступно в `python`. Как уже многократно подмечалось ранее, в `python` всё является объектом. Можно даже сказать больше, любой тип объекта (встроенный или пользовательский) или напрямую или через один из своих базовых классов расширяет тип `object`. \n", "\n", "В частности, следующее объявление класса неявно расширяет `object`." ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "(,)\n" ] }, { "data": { "text/plain": [ "True" ] }, "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ "class MyClass:\n", " pass\n", "\n", "print(MyClass.__bases__)\n", "issubclass(MyClass, object)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Специальный атрибут [\\_\\_bases\\_\\_](https://docs.python.org/3/library/stdtypes.html#class.__bases__) позволяет узнать базовый класс (или классы, `python` поддерживает множественное наследование). Встроенная функция [issubclass](https://docs.python.org/3/library/functions.html#issubclass) возвращает `True`, отвечает на вопрос, является ли класс указанный в первом аргументе производным от класса указанного во втором аргументе. \n", "\n", "## Базовый синтаксис\n", "\n", "Допустим, у нас есть базовый класс `BaseClass` и мы хотим объявить класс `DerivedClass`, который будет расширять его. Тогда необходимо указать базовый класс в заголовочной строке производного класса в круглых скобках после имени базового класса." ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "(__main__.BaseClass,)" ] }, "execution_count": 12, "metadata": {}, "output_type": "execute_result" } ], "source": [ "class BaseClass:\n", " pass\n", "\n", "class DerivedClass(BaseClass):\n", " pass\n", "\n", "DerivedClass.__bases__" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "В данном примере заголовок `class DerivedClass(BaseClass):` сигнализирует, что `DerivedClass` наследует от `BaseClass`. \n", "\n", "```{figure} /_static/lecture_specific/inheritance/single_0.png\n", " :scale: 30%\n", "```\n", "При проверке принадлежности экземпляра производного класса к базовому классу метод [isinstance](https://docs.python.org/3/library/functions.html#isinstance) вернет `True`." ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "True\n", "False\n" ] } ], "source": [ "d = DerivedClass()\n", "print(isinstance(d, BaseClass))\n", "print(type(d) == BaseClass)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Из-за этой особенности, принято проверять принадлежность к классу именно методом `isinstance(obj, cls)`, а не выражением вида `type(obj) == cls`. Это позволяет писать код, который не будет замечать разницы между экземплярами базового и производного классов. В ряде ситуаций область применения такого кода можно будет расширять, не редактируя его.\n", "\n", "## Наследование атрибутов класса\n", "\n", "Кроме того, производный класс наследует атрибуты и методы базового класса (речь пока идет про атрибуты самого класса, а не экземпляра).\n", "\n", "Определим базовый класс с атрибутом `attr` и со статическим (для удобства вызова) методом `method`." ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Метод класса BaseClass\n", "Атрибут класса BaseClass\n" ] } ], "source": [ "class BaseClass:\n", " attr = \"Атрибут класса BaseClass\"\n", "\n", " @staticmethod\n", " def method():\n", " print(\"Метод класса BaseClass\")\n", "\n", "class DerivedClass(BaseClass):\n", " pass\n", "\n", "DerivedClass.method()\n", "print(DerivedClass.attr)\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "```{figure} /_static/lecture_specific/inheritance/single_0.png\n", " :scale: 30%\n", "```\n", "\n", "Видим, что через объект объявления производного класса `DerivedClass` удаётся получить доступ к атрибутам и методам базового класса `BaseClass`. \n", "\n", "
\n", "Под капотом.\n", "\n", "Механизм наследования атрибутов не совсем очевиден. У объекта объявления производного класса не появляются атрибуты базового класса, но получить доступ к атрибуту базового класса через объект объявления производного класса получается из-за механизма поиска атрибутов класса.\n", "\n", "Пусть мы пытаемся выражением `C.x` получить доступ к атрибуту `x` класса `C`, который расширяет класс `B`. Тогда выполняется следующая процедура. \n", "1. Атрибут `x` ищется у класса `C`. Если он обнаруживается, то он и возвращается;\n", "2. Если атрибут `x` у класса `C` найти не удаётся, то атрибут `x` ищется у базового класса `B`.\n", "\n", "Второй шаг выполняется рекурсивно, т.е. если `B` расширяет класс `A` и в `B` тоже не удаётся найти атрибут `x`, то поиск продолжится в классе `A` (и далее по цепочке наследования). \n", "\n", "Доступ к атрибутам базового класса через экземпляр производного класса тоже возможен, т.к. процедура поиска атрибута у экземпляра делегирует этот поиск классу этого экземпляра (см. процедуру выше), если в самом экземпляре нет такого атрибута. \n", "\n", "
\n", "\n", "## Перекрытие атрибутов\n", "\n", "Если в производном классе есть атрибуты с такими же именами, как и в базовом классе, то они переопределят таковые из базового класса.\n", "\n", "Расширим определение производного класса `DerivedClass` из предыдущего примера его собственными атрибутами `attr` и `method`." ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Метод класса DerivedClass\n", "Атрибут класса DerivedClass\n" ] } ], "source": [ "class BaseClass:\n", " attr = \"Атрибут класса BaseClass\"\n", "\n", " @staticmethod\n", " def method():\n", " print(\"Метод класса BaseClass\")\n", "\n", "class DerivedClass(BaseClass):\n", " attr = \"Атрибут класса DerivedClass\"\n", "\n", " @staticmethod\n", " def method():\n", " print(\"Метод класса DerivedClass\")\n", "\n", "DerivedClass.method()\n", "print(DerivedClass.attr)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "```{figure} /_static/lecture_specific/inheritance/single_0.png\n", " :scale: 30%\n", "```\n", "\n", "Видим, что теперь через объект объявления производного класса вызываются его же методы." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Множественное наследование\n", "\n", "Можно наследовать сразу от нескольких базовых классов. Для этого необходимо указать их через запятую.\n" ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [], "source": [ "class LeftBase:\n", " a = \"a left\"\n", " b = \"b left\"\n", "\n", "class MiddleBase:\n", " b = \"b middle\"\n", " c = \"c middle\"\n", "\n", "class RightBase:\n", " c = \"c right\"\n", " d = \"d right\"\n", "\n", "\n", "class DerivedClass(LeftBase, MiddleBase, RightBase):\n", " pass" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "В примере `DerivedClass` наследует сразу от трех классов. \n", "\n", "```{figure} /_static/lecture_specific/inheritance/multi.png\n", " :scale: 20%\n", "```\n", "\n", "\n", "Атрибуты в базовых классах пересекаются: атрибут `b` есть и у `LeftBase` и у `MiddleBase`, атрибут `c` есть и у `MiddleBase` и у `RightBase`. Возникает вопрос, если обратиться от производного класса к этим атрибутам, то значение атрибута какого из базовых классов вернется в качестве результата? Распечатаем атрибуты `a`, `b`, `c` и `d` класса `DerivedClass`." ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "a: a left\n", "b: b left\n", "c: c middle\n", "d: d right\n" ] } ], "source": [ "for attr in \"abcd\":\n", " print(f\"{attr}: {getattr(DerivedClass, attr)}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Видим, что возвращается атрибут того базового класса, который указан в списке базовых классов первым (самый левый).\n", "\n", "## Вызов методов базового класса. Функция `super`\n", "\n", "Иногда все же возникает необходимость вызвать перекрытый метод базового класса в экземпляре производного класса. Яркий пример --- инициализация объекта. При создании объекта необходимо убедиться, что будет вызван и метод `__init__` базового класса и метод `__init__` производного класса. Обычно этого добиваются вызовом инициализирующего метода базового класса в самом начале инициализирующего метода производного класса. \n", "\n", "Неопытному программисту на `python` может показаться, что этого можно добиться следующим образом:\n", "```python\n", "class A:\n", " def __init__(self, x):\n", " self.x = x\n", "\n", "class B(A):\n", " def __init__(self, x, y):\n", " self.__init__(x)\n", " self.y = y\n", "```\n", "Но это приведет к рекурсии: метод `B.__init__` создаётся на этапе объявления класса, а значит при поиске атрибута `self.__init__` найдется именно `B.__init__` (у экземпляра `self` такого атрибута нет, а значит поиск идёт в его классе), а не `A.__init__`. \n", "\n", "Выход из этой ситуации --- вызвать метод `A.__init__` явно.\n", "```python\n", "class A:\n", " def __init__(self, x):\n", " self.x = x\n", "\n", "class B(A):\n", " def __init__(self, x, y):\n", " A.__init__(self, x)\n", " self.y = y\n", "```\n", "При таком подходе произойдет то, чего мы и добивались. Тем не менее принято делать это иначе, а именно использовать встроенную функцию [super](https://docs.python.org/3/library/functions.html#super). В нашем примере, инструкция \n", "```python\n", "A.__init__(self, x)\n", "```\n", "заменяется на\n", "```python\n", "super().__init__(x)\n", "```\n", "\n", "```{note}\n", "Параметр `self` при вызове через `super` передавать не надо!\n", "```" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Инициализация в B\n", "Инициализация в A\n", "42 3.14\n" ] } ], "source": [ "class A:\n", " def __init__(self, x):\n", " print(\"Инициализация в A\")\n", " self.x = x\n", "\n", "class B(A):\n", " def __init__(self, x, y):\n", " print(\"Инициализация в B\")\n", " super().__init__(x)\n", " self.y = y\n", "\n", "\n", "b = B(42, 3.14)\n", "print(b.x, b.y)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "В таком крошечном примере может показаться, что использование такого подхода с функцией `super` ни чем не упрощает вызов методов базового класса. Тем не менее принято предпочитать именно его даже в самых простых ситуациях. В более сложных иерархиях классов без функции `super` сложно обойтись.\n", "\n", "```{figure} /_static/lecture_specific/inheritance/super.png\n", " :scale: 20%\n", "```\n", "Рассмотрим следующую иерархию наследования: `South` наследует от `West` и `East`, каждый из которых в свою очередь расширяют класс `North`. Хочется, чтобы при инициализации экземпляра класса `South` вызывались и методы инициализации всех базовых классов. Попробуем реализовать эту схему, указывая все базовые классы явно. " ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "South\n", "West\n", "North\n", "East\n", "North\n" ] } ], "source": [ "class North:\n", " def __init__(self):\n", " print(\"North\")\n", "\n", "class West(North):\n", " def __init__(self):\n", " print(\"West\")\n", " North.__init__(self)\n", " \n", "class East(North):\n", " def __init__(self):\n", " print(\"East\")\n", " North.__init__(self)\n", " \n", "class South(West, East):\n", " def __init__(self):\n", " print(\"South\")\n", " West.__init__(self)\n", " East.__init__(self)\n", " \n", "s = South()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Видим, что метод инициализации класса `North` вызвался дважды. Первый раз это произошло через класс `West`, а второй раз через класс `East`.\n", "Теперь заменим все явные упоминания классов через функцию `super`." ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "South\n", "West\n", "East\n", "Up\n" ] } ], "source": [ "class North:\n", " def __init__(self):\n", " print(\"Up\")\n", "\n", "class West(North):\n", " def __init__(self):\n", " print(\"West\")\n", " super().__init__()\n", " \n", "class East(North):\n", " def __init__(self):\n", " print(\"East\")\n", " super().__init__()\n", " \n", "class South(West, East):\n", " def __init__(self):\n", " print(\"South\")\n", " super().__init__()\n", " \n", "\n", "s = South()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Проблема с тем, что метод инициализации `North` вызывался дважды, решена! Функция `super` использует [С3-линеаризацию](https://ru.wikipedia.org/wiki/C3-%D0%BB%D0%B8%D0%BD%D0%B5%D0%B0%D1%80%D0%B8%D0%B7%D0%B0%D1%86%D0%B8%D1%8F) ([method resolution order](https://www.python.org/download/releases/2.3/mro/)) для определения порядка, в котором вызывать методы классов в иерархии наследования. Но чтобы это работало, необходимо, чтобы везде вызов происходил именно через `super`." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "(abc)=\n", "## Абстрактный базовый класс. Абстрактный метод.\n", "\n", "Модуль [abc](https://docs.python.org/3/library/abc.html) (сокращение от `Abstract Base Class`) предоставляет инструменты для реализации абстрактных базовых классов, т.е. классов, которые лишь задают интерфейс и не предназначены для создания экземпляров напрямую. Обычно, абстрактный базовый класс наследует от [abc.ABC](https://docs.python.org/3/library/abc.html#abc.ABC), а абстрактные методы помечаются декоратором [abc.abstractmethod](https://docs.python.org/3/library/abc.html#abc.abstractmethod). Производные от такого абстрактного базового класса классы смогут создавать экземпляры, только если они переопределят все абстрактные методы. Если не переопределен хоть один из абстрактных методов, то `python` возбудит ошибку при попытке создать экземпляр. Так как тело абстрактной функции не играет никакой роли, то в нем часто возбуждают исключение [NotImplementedError](https://docs.python.org/3/library/exceptions.html#NotImplementedError).\n", "\n", "В качестве примера реализуем абстрактный базовый класс `Shape` для геометрической фигуры. Как и в примере с треугольником, будем считать, что каждая фигура должна уметь считать свой периметр и площадь. " ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Can't instantiate abstract class Shape with abstract methods area, perimeter\n" ] } ], "source": [ "from abc import ABC, abstractmethod\n", "\n", "class Shape(ABC):\n", " @abstractmethod\n", " def perimeter(self):\n", " raise NotImplementedError\n", " \n", " @abstractmethod\n", " def area(self):\n", " raise NotImplementedError\n", " \n", "\n", "try:\n", " Shape()\n", "except TypeError as msg:\n", " print(msg)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Видим, что экземпляр класса `Shape` создать не удаётся --- у него есть абстрактные методы." ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Can't instantiate abstract class Circle with abstract methods area\n" ] } ], "source": [ "from math import pi\n", "\n", "class Circle(Shape):\n", " def __init__(self, radius):\n", " self.radius = radius\n", "\n", " def perimeter(self):\n", " return 2 * pi * self.radius\n", "\n", "try:\n", " Circle()\n", "except TypeError as msg:\n", " print(msg)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Переопределение лишь одного метода не меняет ситуацию. Из сообщения ошибки можно понять, какие методы мы забыли доопределить." ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "3.141592653589793 6.283185307179586\n" ] } ], "source": [ "from math import pi\n", "\n", "class Circle(Shape):\n", " def __init__(self, radius):\n", " self.radius = radius\n", "\n", " def perimeter(self):\n", " return 2 * pi * self.radius\n", "\n", " def area(self):\n", " return pi * self.radius * self.radius\n", "\n", "c = Circle(1)\n", "print(c.area(), c.perimeter())" ] } ], "metadata": { "interpreter": { "hash": "cd49b4596bae9c980ff74fdf93e8fe80e447435ae307c062fad6c4f9ef2eb47f" }, "kernelspec": { "display_name": "Python 3.8.10 64-bit ('venv': venv)", "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.10" }, "orig_nbformat": 4 }, "nbformat": 4, "nbformat_minor": 2 }