Полиморфизм в python
Contents
Полиморфизм в python#
Полиморфизм в python
реализован во многих ипостасях.
Ad-hoc полиморфизм#
Ad-hoc полиморфизм пример полиморфизма, который не свойственен для python
, т.к. перегружать функции честным образом в нем нельзя: каждое следующее объявление функции с таким же именем затрет предыдущее.
Рассмотрим пример такого полиморфизма в C++
.
#include <vector>
#include <iostream>
int double_it(int x){
return 2*x;
}
std::vector<int> double_it(const std::vector<int> &x){
size_t n = x.size();
std::vector<int> result(n);
for(int i=0; i < n; ++i)
result[i] = 2*x[i];
return result;
}
int main(){
int int_result = double_it(42);
std::vector<int> vector_result = double_it({1, 2, 3});
std::cout << int_result << std::endl; // Вывод: 84
for(auto x: vector_result)
std::cout << x << " "; // Вывод: 2 4 6
return 0;
}
В данном примере функция double_it
как бы векторизована: её можно вызывать и для одного целого числа и сразу для вектора целых чисел.
В python
нельзя перегружать функции по типу параметров. Лучшее что вы можете сделать — проверить в runtime, какого типа аргумент, и в соответствии с этим проделать необходимые операции.
def double_it(x):
if isinstance(x, int):
return 2 * x
if isinstance(x, list):
return [double_it(value) for value in x]
raise TypeError(f"Функция double_it вызвана с неподдерживаемым типом аргумента {type(x).__name__}")
print(double_it(1))
print(double_it([1, 2, 3]))
2
[2, 4, 6]
В данном примере, если аргумент не целое число и не список, то возбуждается исключение TypeError
, чтобы в наибольшей степени воспроизвести поведение кода из C++
.
singledispatch
#
В ряде ситуаций удобно применить singledispatch из модуля functools, который позволяет как бы перегружать функции по типу первого аргумента.
from functools import singledispatch
@singledispatch
def double_it(x):
"""
Generic function.
Общая функция.
Она будет вызвана,
если тип аргумента не соответствует ни одному зарегистрированному.
"""
raise TypeError(f"Функция double_it вызвана с неподдерживаемым типом аргумента {type(x).__name__}")
@double_it.register(int)
def _(x):
return 2 * x
@double_it.register(list)
def _(x):
return [double_it(value) for value in x]
print(double_it(1))
print(double_it([1, 2, 3]))
2
[2, 4, 6]
Сначала объявляется и декорируется наиболее общая функция, которую называют generic
. Это приводит к тому, что создаётся объект-обертка с именем этой функции, у которого есть метод-декоратор register
. С помощью него регистрируются все остальные реализации этой функции для разного типа первого параметра.
Note
Перегруженные версии функции объявляются с именем, отличным от имени исходной функции. Обычно это "_"
.
При вызове функции объект-обертка сравнивает тип первого параметра с зарегистрированными функциями и на основе этой информации вызывает подходящую реализацию. Если подходящей реализация не находится, то вызывается исходная generic
функция.
print(double_it.registry.keys())
try:
double_it(1.)
except TypeError as msg:
print(msg)
dict_keys([<class 'object'>, <class 'int'>, <class 'list'>])
Функция double_it вызвана с неподдерживаевомым типом аргумента float
Note
Декоратора для множественной диспетчеризации в стандартной библиотеке python
нет, но существует сторонняя библиотека multidispatch.
Параметрический полиморфизм#
Параметрический полиморфизм в C++
реализован через шаблонные функции.
#include <iostream>
#include <string>
template<class T>
T add(T x, T y){
return x + y;
}
int main(){
int x1 = 2, y1 = 2;
double x2 = 3.14, y2 = 2.71;
std::string x3 = "Hello, ", y3 = "World!";
std::cout << "int : " << add(x1, y1) << std::endl; // int : 4
std::cout << "double: " << add(x2, y2) << std::endl; // double: 5.85
std::cout << "string: " << add(x3, y3) << std::endl; // string: Hello, World!
return 0;
}
Компилятор сможет скомпилировать реализацию шаблонной функции add
для произвольного типа данных, который поддерживает оператор +
. Таким образом, проектируя шаблонную функцию, программист не ориентируется на конкретный тип данных, а лишь на интерфейс или поведение этого типа данных. Выходит, что как бы один и тот же код может обрабатывать сущности разных типов.
Note
Компилятор C++
сгенерирует свою версию шаблонной функции для каждого типа данных, с котором встретит вызов это функции, что может привести к кратному увеличению объёма получаемого машинного кода. Такой эффект получил известность как «раздувание кода».
В итоге формально каждый тип данных обрабатывается своим собственным отдельным куском кода, что понижает полиморфность этого приёма.
В python
большинство функций можно считать параметрически полиморфными. В функцию add
ниже можно передать аргументы любых типов. Функция успешно вернет значение для любых аргументов, для которых выражение x + y
не возбуждает ошибку. При этом в отличие от примера с C++
эту функцию можно вызывать с аргументами разных типов.
def add(x, y):
return x + y
x1, y1 = 2, 2
x2, y2 = 3.14, 2.71
x3, y3 = "Hello, ", "World!"
print(f"int : {add(x1, y1)}")
print(f"double: {add(x2, y2)}")
print(f"string: {add(x3, y3)}")
int : 4
double: 5.85
string: Hello, World!
Итого, проектируя функцию вы всегда ориентируетесь лишь на интерфейс объектов, которые будут в неё переданы. Часто говорят, что в python
работает утиная типизация.
Полиморфизм подтипа#
Полиморфизм подтипа в C++
реализован через виртуальные методы.
#include <string>
#include <iostream>
#include <math.h>
class Shape{
public:
virtual double area() const = 0;
};
class Circle: public Shape
{
double radius;
public:
Circle(double r) : radius(r) {};
virtual double area() const {return M_PI * radius * radius;}
};
class Square: public Shape
{
double side;
public:
Square(double l) : side(l){};
virtual double area() const {return side * side;}
};
void print_area(const Shape* p){
std::cout << "The area of the shape is " << p->area() << std::endl;
}
int main(){
Circle A(1.);
Square B(2.);
print_area(&A); // The area of the shape is 3.14159
print_area(&B); // The area of the shape is 4
return 0;
}
Здесь функция print_area
заранее не знает, какую конкретную реализацию метода area
она будет вызывать. Это определяется в runtime
в зависимости от истинного типа объекта по указателю. В итоге функция print_area
может обрабатывать все подклассы базового класса Shape
одинаково, что и считается полиморфным поведением. При этом в отличие от шаблонных функций для виртуальных функций не генерируется свой машинный код для каждого подкласса, все по-настоящему обрабатывается одним и тем же кодом.
В python
почти все методы класса проявляют виртуальные свойства. Воспроизведем пример из C++
в python
.
from math import pi
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
raise NotImplementedError
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return pi * self.radius * self.radius
class Square(Shape):
def __init__(self, side):
self.side = side
def area(self):
return self.side * self.side
def print_area(shape):
print(f"The area of the shape is {shape.area()}")
print_area(Circle(1.))
print_area(Square(2.))
The area of the shape is 3.141592653589793
The area of the shape is 4.0
Как уже обсуждалось, в python
обычно роль играет лишь интерфейс или поведение объекта, а не его тип, а значит предыдущий пример мог быть реализован и без наследования: можно было объявить классы Circle
и Square
без базовых классов, тогда функция print_area
работала бы так же, если бы у обоих классов реализован метод area
.
Абстрактные базовые классы, как средство достижения полиморфизма#
Тем не менее полиморфизм подтипа полезен в python
не только, чтобы явно обозначить иерархию между типами данных, или, чтобы напомнить программисту реализовать определенный интерфейс, но и для более совершенной реализации Ad-hoc полиморфизма. Это достигается за счет того, что встроенная функция isinstance возвращает True
и для объектов производных классов.
В качестве примера рассмотрим рассмотренную выше реализацию функции double_it
.
def double_it(x):
if isinstance(x, int):
return 2 * x
if isinstance(x, list):
return [double_it(value) for value in x]
raise TypeError(f"Функция double_it вызвана с неподдерживаемым типом аргумента {type(x).__name__}")
print(double_it(1))
print(double_it([1, 2, 3]))
try:
double_it(1.)
except TypeError as msg:
print(msg)
2
[2, 4, 6]
Функция double_it вызвана с неподдерживаевомым типом аргумента float
Обратим внимание, что она работает только для целых чисел или списка целых чисел, хотя такие ограничения кажутся слишком специфичными: для python
умножить действительное число на два ничуть не сложнее, чем целое число. Т.е. запросом на принадлежность объекта классу целых чисел мы сильно заузили область применения.
К счастью, для таких целей существуют абстрактный базовый класс Number из модуля numbers. Все встроенные числовые типы данных наследует от этого базового класса.
from numbers import Number
from decimal import Decimal
from fractions import Fraction
types = [bool, int, Fraction, Decimal, float, complex]
for t in types:
if issubclass(t, Number):
print(f"Тип {t.__name__} расширяет класс Number.")
Тип bool расширяет класс Number.
Тип int расширяет класс Number.
Тип Fraction расширяет класс Number.
Тип Decimal расширяет класс Number.
Тип float расширяет класс Number.
Тип complex расширяет класс Number.
А модуль collections кроме очень полезных контейнеров определяет абстрактные базовые классы для коллекций в подмодуле collections.abc. Для целей примера выше сгодится абстрактный базовый класс Collection.
from collections.abc import Collection
from array import array
types = [tuple, list, array, str, dict]
for t in types:
if issubclass(t, Collection):
print(f"Тип {t.__name__} расширяет класс Collection.")
Тип tuple расширяет класс Collection.
Тип list расширяет класс Collection.
Тип array расширяет класс Collection.
Тип str расширяет класс Collection.
Тип dict расширяет класс Collection.
Отредактируем исходный пример, чтобы он работал с любыми числами и любыми коллекциями чисел.
from collections.abc import Collection
from numbers import Number
from fractions import Fraction
def double_it(x):
if isinstance(x, Number):
return 2 * x
if isinstance(x, Collection) and not isinstance(x, str):
return [double_it(value) for value in x]
raise TypeError(f"Функция double_it вызвана с неподдерживаемым типом аргумента {type(x).__name__}")
print(double_it(
[
1, # int
2., # float
3. + 4.j, # complex
Fraction(5, 6), # Fraction
{9., 10.} # set of floats
]
))
[2, 4.0, (6+8j), Fraction(5, 3), [18.0, 20.0]]