{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Полиморфизм в python\n", "\n", "[Полиморфизм](https://ru.wikipedia.org/wiki/%D0%9F%D0%BE%D0%BB%D0%B8%D0%BC%D0%BE%D1%80%D1%84%D0%B8%D0%B7%D0%BC_(%D0%B8%D0%BD%D1%84%D0%BE%D1%80%D0%BC%D0%B0%D1%82%D0%B8%D0%BA%D0%B0)) в `python` реализован во многих ипостасях.\n", "\n", "## Ad-hoc полиморфизм\n", "\n", "[Ad-hoc полиморфизм](https://ru.wikipedia.org/wiki/%D0%9F%D0%BE%D0%BB%D0%B8%D0%BC%D0%BE%D1%80%D1%84%D0%B8%D0%B7%D0%BC_(%D0%B8%D0%BD%D1%84%D0%BE%D1%80%D0%BC%D0%B0%D1%82%D0%B8%D0%BA%D0%B0)#%D0%A1%D0%BF%D0%B5%D1%86%D0%B8%D0%B0%D0%BB%D1%8C%D0%BD%D1%8B%D0%B9_%D0%BF%D0%BE%D0%BB%D0%B8%D0%BC%D0%BE%D1%80%D1%84%D0%B8%D0%B7%D0%BC) пример полиморфизма, который не свойственен для `python`, т.к. [перегружать функции](https://ru.wikipedia.org/wiki/%D0%9F%D0%B5%D1%80%D0%B5%D0%B3%D1%80%D1%83%D0%B7%D0%BA%D0%B0_%D0%BF%D1%80%D0%BE%D1%86%D0%B5%D0%B4%D1%83%D1%80_%D0%B8_%D1%84%D1%83%D0%BD%D0%BA%D1%86%D0%B8%D0%B9) честным образом в нем нельзя: каждое следующее объявление функции с таким же именем затрет предыдущее.\n", "\n", "Рассмотрим пример такого полиморфизма в `C++`.\n", "\n", "```c++\n", "#include \n", "#include \n", "\n", "int double_it(int x){\n", " return 2*x;\n", "}\n", "\n", "std::vector double_it(const std::vector &x){\n", " size_t n = x.size();\n", "\tstd::vector result(n);\n", "\t\n", "\tfor(int i=0; i < n; ++i)\n", "\t\tresult[i] = 2*x[i];\n", "\t\n", "\treturn result;\n", "}\n", "\n", "int main(){\n", "\tint int_result = double_it(42);\n", "\tstd::vector vector_result = double_it({1, 2, 3});\n", "\t\n", "\tstd::cout << int_result << std::endl; // Вывод: 84\n", "\tfor(auto x: vector_result)\n", "\t\tstd::cout << x << \" \"; // Вывод: 2 4 6\n", "\t\n", "\treturn 0;\n", "}\n", "```\n", "В данном примере функция `double_it` как бы векторизована: её можно вызывать и для одного целого числа и сразу для вектора целых чисел. \n", "\n", "В `python` нельзя перегружать функции по типу параметров. Лучшее что вы можете сделать --- проверить в *runtime*, какого типа аргумент, и в соответствии с этим проделать необходимые операции." ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "2\n", "[2, 4, 6]\n" ] } ], "source": [ "def double_it(x):\n", " if isinstance(x, int):\n", " return 2 * x\n", " if isinstance(x, list):\n", " return [double_it(value) for value in x]\n", " raise TypeError(f\"Функция double_it вызвана с неподдерживаемым типом аргумента {type(x).__name__}\")\n", "\n", "\n", "print(double_it(1))\n", "print(double_it([1, 2, 3]))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "В данном примере, если аргумент не целое число и не список, то возбуждается исключение `TypeError`, чтобы в наибольшей степени воспроизвести поведение кода из `C++`. " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### `singledispatch`\n", "\n", "В ряде ситуаций удобно применить [singledispatch](https://docs.python.org/3/library/functools.html#functools.singledispatch) из модуля [functools](https://docs.python.org/3/library/functools.html#module-functools), который позволяет как бы перегружать функции по типу первого аргумента." ] }, { "cell_type": "code", "execution_count": 16, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "2\n", "[2, 4, 6]\n" ] } ], "source": [ "from functools import singledispatch\n", "\n", "@singledispatch\n", "def double_it(x):\n", " \"\"\"\n", " Generic function.\n", " Общая функция.\n", " Она будет вызвана, \n", " если тип аргумента не соответствует ни одному зарегистрированному. \n", " \"\"\"\n", " raise TypeError(f\"Функция double_it вызвана с неподдерживаемым типом аргумента {type(x).__name__}\")\n", "\n", "@double_it.register(int)\n", "def _(x):\n", " return 2 * x\n", "\n", "\n", "@double_it.register(list)\n", "def _(x):\n", " return [double_it(value) for value in x]\n", "\n", "print(double_it(1))\n", "print(double_it([1, 2, 3]))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Сначала объявляется и декорируется наиболее общая функция, которую называют `generic`. Это приводит к тому, что создаётся объект-обертка с именем этой функции, у которого есть метод-декоратор `register`. С помощью него регистрируются все остальные реализации этой функции для разного типа первого параметра. \n", "\n", "```{note}\n", "Перегруженные версии функции объявляются с именем, отличным от имени исходной функции. Обычно это `\"_\"`.\n", "```\n", "\n", "При вызове функции объект-обертка сравнивает тип первого параметра с зарегистрированными функциями и на основе этой информации вызывает подходящую реализацию. Если подходящей реализация не находится, то вызывается исходная `generic` функция. " ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "dict_keys([, , ])\n", "Функция double_it вызвана с неподдерживаевомым типом аргумента float\n" ] } ], "source": [ "print(double_it.registry.keys())\n", "\n", "try:\n", " double_it(1.)\n", "except TypeError as msg:\n", " print(msg)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "```{note}\n", "Декоратора для [множественной диспетчеризации](https://ru.wikipedia.org/wiki/%D0%9C%D1%83%D0%BB%D1%8C%D1%82%D0%B8%D0%BC%D0%B5%D1%82%D0%BE%D0%B4) в стандартной библиотеке `python` нет, но существует сторонняя библиотека [multidispatch](https://multiple-dispatch.readthedocs.io/en/latest/). \n", "```\n", "\n", "## Параметрический полиморфизм\n", "\n", "[Параметрический полиморфизм](https://ru.wikipedia.org/wiki/%D0%9F%D0%B0%D1%80%D0%B0%D0%BC%D0%B5%D1%82%D1%80%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%B8%D0%B9_%D0%BF%D0%BE%D0%BB%D0%B8%D0%BC%D0%BE%D1%80%D1%84%D0%B8%D0%B7%D0%BC) в `C++` реализован через шаблонные функции. \n", "\n", "\n", "```c++\n", "#include \n", "#include \n", "\n", "template\n", "T add(T x, T y){\n", "\treturn x + y;\n", "}\n", "\n", "int main(){\n", "\tint x1 = 2, y1 = 2;\n", "\tdouble x2 = 3.14, y2 = 2.71;\n", "\tstd::string x3 = \"Hello, \", y3 = \"World!\";\n", "\t\n", "\tstd::cout << \"int : \" << add(x1, y1) << std::endl; // int : 4\n", "\tstd::cout << \"double: \" << add(x2, y2) << std::endl; // double: 5.85\n", "\tstd::cout << \"string: \" << add(x3, y3) << std::endl; // string: Hello, World!\n", "\t\n", "\treturn 0;\n", "}\n", "```\n", "\n", "Компилятор сможет скомпилировать реализацию шаблонной функции `add` для произвольного типа данных, который поддерживает оператор `+`. Таким образом, проектируя шаблонную функцию, программист не ориентируется на конкретный тип данных, а лишь на интерфейс или поведение этого типа данных. Выходит, что как бы один и тот же код может обрабатывать сущности разных типов.\n", "\n", "```{note}\n", "Компилятор `C++` сгенерирует свою версию шаблонной функции для каждого типа данных, с котором встретит вызов это функции, что может привести к кратному увеличению объёма получаемого машинного кода. Такой эффект получил известность как «раздувание кода».\n", "\n", "В итоге формально каждый тип данных обрабатывается своим собственным отдельным куском кода, что понижает полиморфность этого приёма. \n", "```\n", "\n", "В `python` большинство функций можно считать параметрически полиморфными. В функцию `add` ниже можно передать аргументы любых типов. Функция успешно вернет значение для любых аргументов, для которых выражение `x + y` не возбуждает ошибку. При этом в отличие от примера с `C++` эту функцию можно вызывать с аргументами разных типов." ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "int : 4\n", "double: 5.85\n", "string: Hello, World!\n" ] } ], "source": [ "def add(x, y):\n", " return x + y\n", "\n", "\n", "x1, y1 = 2, 2\n", "x2, y2 = 3.14, 2.71\n", "x3, y3 = \"Hello, \", \"World!\" \n", "\n", "\n", "print(f\"int : {add(x1, y1)}\")\n", "print(f\"double: {add(x2, y2)}\")\n", "print(f\"string: {add(x3, y3)}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Итого, проектируя функцию вы всегда ориентируетесь лишь на интерфейс объектов, которые будут в неё переданы. Часто говорят, что в `python` работает [утиная типизация](https://ru.wikipedia.org/wiki/%D0%A3%D1%82%D0%B8%D0%BD%D0%B0%D1%8F_%D1%82%D0%B8%D0%BF%D0%B8%D0%B7%D0%B0%D1%86%D0%B8%D1%8F).\n", "\n", "## Полиморфизм подтипа\n", "\n", "[Полиморфизм подтипа](https://ru.wikipedia.org/wiki/%D0%9F%D0%BE%D0%BB%D0%B8%D0%BC%D0%BE%D1%80%D1%84%D0%B8%D0%B7%D0%BC_(%D0%B8%D0%BD%D1%84%D0%BE%D1%80%D0%BC%D0%B0%D1%82%D0%B8%D0%BA%D0%B0)#%D0%9F%D0%BE%D0%BB%D0%B8%D0%BC%D0%BE%D1%80%D1%84%D0%B8%D0%B7%D0%BC_%D0%BF%D0%BE%D0%B4%D1%82%D0%B8%D0%BF%D0%BE%D0%B2) в `C++` реализован через [виртуальные методы](https://ru.wikipedia.org/wiki/%D0%92%D0%B8%D1%80%D1%82%D1%83%D0%B0%D0%BB%D1%8C%D0%BD%D1%8B%D0%B9_%D0%BC%D0%B5%D1%82%D0%BE%D0%B4).\n", "\n", "```c++\n", "#include \n", "#include \n", "#include \n", "\n", "class Shape{\n", "public:\n", "\tvirtual double area() const = 0;\n", "};\n", "\n", "class Circle: public Shape\n", "{\n", "\tdouble radius;\n", "public:\t\n", "\tCircle(double r) : radius(r) {};\n", "\tvirtual double area() const {return M_PI * radius * radius;}\n", "\t\n", "};\n", "\n", "class Square: public Shape\n", "{\n", "\tdouble side;\n", "public:\n", "\tSquare(double l) : side(l){};\n", "\tvirtual double area() const {return side * side;}\n", "\t\n", "};\n", "\n", "void print_area(const Shape* p){\n", "\tstd::cout << \"The area of the shape is \" << p->area() << std::endl;\n", "}\n", "\n", "int main(){\n", "\tCircle A(1.);\n", "\tSquare B(2.);\n", "\t\n", "\tprint_area(&A); // The area of the shape is 3.14159\n", "\tprint_area(&B); // The area of the shape is 4\n", "\treturn 0;\n", "}\n", "```\n", "\n", "Здесь функция `print_area` заранее не знает, какую конкретную реализацию метода `area` она будет вызывать. Это определяется в `runtime` в зависимости от истинного типа объекта по указателю. В итоге функция `print_area` может обрабатывать все подклассы базового класса `Shape` одинаково, что и считается полиморфным поведением. При этом в отличие от шаблонных функций для виртуальных функций не генерируется свой машинный код для каждого подкласса, все по-настоящему обрабатывается одним и тем же кодом.\n", "\n", "В `python` почти все методы класса проявляют виртуальные свойства. Воспроизведем пример из `C++` в `python`." ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "The area of the shape is 3.141592653589793\n", "The area of the shape is 4.0\n" ] } ], "source": [ "from math import pi\n", "from abc import ABC, abstractmethod\n", "\n", "class Shape(ABC):\n", " @abstractmethod\n", " def area(self):\n", " raise NotImplementedError\n", "\n", "\n", "class Circle(Shape):\n", " def __init__(self, radius):\n", " self.radius = radius\n", "\n", " def area(self):\n", " return pi * self.radius * self.radius\n", "\n", "class Square(Shape):\n", " def __init__(self, side):\n", " self.side = side\n", "\n", " def area(self):\n", " return self.side * self.side\n", "\n", "def print_area(shape):\n", " print(f\"The area of the shape is {shape.area()}\")\n", "\n", "print_area(Circle(1.))\n", "print_area(Square(2.))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Как уже обсуждалось, в `python` обычно роль играет лишь интерфейс или поведение объекта, а не его тип, а значит предыдущий пример мог быть реализован и без наследования: можно было объявить классы `Circle` и `Square` без базовых классов, тогда функция `print_area` работала бы так же, если бы у обоих классов реализован метод `area`.\n", "\n", "### Абстрактные базовые классы, как средство достижения полиморфизма \n", "\n", "Тем не менее полиморфизм подтипа полезен в `python` не только, чтобы явно обозначить иерархию между типами данных, или, чтобы напомнить программисту реализовать определенный интерфейс, но и для более совершенной реализации *Ad-hoc* полиморфизма. Это достигается за счет того, что встроенная функция [isinstance](https://docs.python.org/3/library/functions.html#isinstance) возвращает `True` и для объектов производных классов.\n", "\n", "В качестве примера рассмотрим рассмотренную выше реализацию функции `double_it`. " ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "2\n", "[2, 4, 6]\n", "Функция double_it вызвана с неподдерживаевомым типом аргумента float\n" ] } ], "source": [ "def double_it(x):\n", " if isinstance(x, int):\n", " return 2 * x\n", " if isinstance(x, list):\n", " return [double_it(value) for value in x]\n", " raise TypeError(f\"Функция double_it вызвана с неподдерживаемым типом аргумента {type(x).__name__}\")\n", "\n", "\n", "print(double_it(1))\n", "print(double_it([1, 2, 3]))\n", "\n", "\n", "try: \n", " double_it(1.)\n", "except TypeError as msg:\n", " print(msg)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Обратим внимание, что она работает только для целых чисел или списка целых чисел, хотя такие ограничения кажутся слишком специфичными: для `python` умножить действительное число на два ничуть не сложнее, чем целое число. Т.е. запросом на принадлежность объекта классу целых чисел мы сильно заузили область применения. \n", "\n", "К счастью, для таких целей существуют абстрактный базовый класс [Number](https://docs.python.org/3/library/numbers.html#numbers.Number) из модуля [numbers](https://docs.python.org/3/library/numbers.html#numbers.Number). Все встроенные числовые типы данных наследует от этого базового класса." ] }, { "cell_type": "code", "execution_count": 20, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Тип bool расширяет класс Number.\n", "Тип int расширяет класс Number.\n", "Тип Fraction расширяет класс Number.\n", "Тип Decimal расширяет класс Number.\n", "Тип float расширяет класс Number.\n", "Тип complex расширяет класс Number.\n" ] } ], "source": [ "from numbers import Number\n", "from decimal import Decimal\n", "from fractions import Fraction\n", "\n", "types = [bool, int, Fraction, Decimal, float, complex]\n", "\n", "for t in types:\n", " if issubclass(t, Number):\n", " print(f\"Тип {t.__name__} расширяет класс Number.\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "А модуль [collections](https://docs.python.org/3/library/collections.abc.html#collections.abc.Sequence) кроме очень полезных контейнеров определяет абстрактные базовые классы для коллекций в подмодуле [collections.abc](https://docs.python.org/3/library/collections.abc.html#collections-abstract-base-classes-detailed-descriptions). Для целей примера выше сгодится абстрактный базовый класс [Collection](https://docs.python.org/3/library/collections.abc.html#collections.abc.Collection)." ] }, { "cell_type": "code", "execution_count": 21, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Тип tuple расширяет класс Collection.\n", "Тип list расширяет класс Collection.\n", "Тип array расширяет класс Collection.\n", "Тип str расширяет класс Collection.\n", "Тип dict расширяет класс Collection.\n" ] } ], "source": [ "from collections.abc import Collection\n", "from array import array\n", "\n", "types = [tuple, list, array, str, dict]\n", "\n", "for t in types:\n", " if issubclass(t, Collection):\n", " print(f\"Тип {t.__name__} расширяет класс Collection.\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Отредактируем исходный пример, чтобы он работал с любыми числами и любыми коллекциями чисел." ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "[2, 4.0, (6+8j), Fraction(5, 3), [18.0, 20.0]]\n" ] } ], "source": [ "from collections.abc import Collection\n", "from numbers import Number\n", "from fractions import Fraction\n", "\n", "def double_it(x):\n", " if isinstance(x, Number):\n", " return 2 * x\n", " if isinstance(x, Collection) and not isinstance(x, str):\n", " return [double_it(value) for value in x]\n", " raise TypeError(f\"Функция double_it вызвана с неподдерживаемым типом аргумента {type(x).__name__}\")\n", "\n", "\n", "print(double_it(\n", " [\n", " 1, # int \n", " 2., # float\n", " 3. + 4.j, # complex\n", " Fraction(5, 6), # Fraction\n", " {9., 10.} # set of floats\n", " ]\n", ")) \n" ] } ], "metadata": { "interpreter": { "hash": "cd49b4596bae9c980ff74fdf93e8fe80e447435ae307c062fad6c4f9ef2eb47f" }, "kernelspec": { "display_name": "Python 3.8.10 ('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 }