Полиморфизм - что это такое?

Приветствую всех на еще одном внеочередном занятии по ООП. И здесь я хочу затронуть еще один вопрос, который волнует моих подписчиков: что такое полиморфизм в ООП на Python? Если попытаться ответить в двух словах на этот вопрос, то можно было бы сказать так:

Это возможность работы с совершенно разными объектами языка Python единым образом.

Кажется, не особо стало понятнее? Поэтому давайте, как всегда, постигнем суть этого подхода на конкретном примере.

Вначале я продемонстрирую пример, где мы увидим один недостаток, который как раз исправляется с помощью полиморфизма. Предположим, у нас есть два класса Rectangle и Square:

class Rectangle:
    def __init__(self, w, h):
        self.w = w
        self.h = h
 
    def getPerRect(self):
        return 2*(self.w+self.h)
 
 
class Square:
    def __init__(self, a):
        self.a = a
 
    def getPerSq(self):
        return 4*self.a

И в них объявлены геттеры getPerRect и getPerSq для получения периметра соответствующих фигур: прямоугольника и квадрата. Далее, мы можем создать экземпляры этих классов и вывести в консоль значения периметров:

r1 = Rectangle(1, 2)
r2 = Rectangle(3, 4)
 
print(r1.getPerRect(), r2.getPerRect())
 
s1 = Square(10)
s2 = Square(20)
 
print(s1.getPerSq(), s2.getPerSq())

Все отлично, все работает. Но, теперь предположим, что все эти объекты помещаются в коллекцию:

geom = [r1, r2, s1, s2]

которую можно легко перебрать с помощью цикла for и где бы мы хотели получить значение периметра для каждой фигуры:

for g in geom:
    print( g.getPerRect() )

Как вы понимаете, когда в цикле очередь дойдет до объекта s1, возникнет ошибка, т.к. в классе Square отсутствует метод getPerRect. Конечно, зная, что в коллекции находятся объекты Rectangle и Square, можно было бы в цикле записать проверку:

for g in geom:
    if isinstance(g, Rectangle):
        print( g.getPerRect() )
    else:
        print(g.getPerSq())

и все заработает. Но у такого кода мало гибкости и, например, при добавлении еще одного класса:

class Triangle:
    def __init__(self, a, b, c):
        self.a = a
        self.b = b
        self.c = c
 
    def getPerTr(self):
        return self.a + self.b + self.c

Получим снова ошибку:

t1 = Triangle(1,2,3)
t2 = Triangle(4,5,6)
geom = [r1, r2, s1, s2, t1, t2]

Конечно, в цикле for можно дополнительно проверить на соответствие классам Square и Triangle, но красоты и гибкости нашей программе это не придаст. Вот как раз здесь очень хорошо применим подход, который и называется полиморфизмом. Мы договоримся в каждом классе создавать методы с одинаковыми именами, например,

getPerimetr

Тогда в цикле будем просто обращаться к этому методу и получать периметры соответствующих фигур:

for g in geom:
    print( g.getPerimetr() )

То есть, Python «понимает» объект какого класса хранится в коллекции и соответственно из этого класса берет метод getPerimetr. Это и есть полиморфизм в действии, когда к разным объектам происходит обращение по одному и тому же имени метода и на выходе получаем разное поведение этой функции.