Полиморфизм

Допустим, у нас есть собака, кошка и утка, и нужно у каждой вызвать speak(). Без полиморфизма получится примерно так:

Python 3.13
for animal in animals:
    if isinstance(animal, Dog):
        print(animal.bark())
    elif isinstance(animal, Cat):
        print(animal.meow())
    elif isinstance(animal, Duck):
        print(animal.quack())

И каждый раз, когда появляется новый тип животного, нужно дописать ещё одну elif-ветку. Код, который знает обо всех возможных типах, не способен расширяться без переписывания.

Полиморфизм — это идея «вызывай одно и то же, а класс пусть сам решает, как реагировать». Тогда тот же код превращается в:

Python 3.13
for animal in animals:
    print(animal.speak())

Dog.speak() возвращает «Гав!», Cat.speak() — «Мяу!», Duck.speak() — «Кря!». Один вызов, разное поведение в зависимости от объекта.

Полиморфизм через наследование

Это самый частый случай: общий родительский класс задаёт «контракт» (какие методы должны быть у потомков), а каждый потомок реализует их по-своему.

Python 3.13
class Animal:
    def __init__(self, name):
        self.name = name

    def speak(self):
        return f"{self.name} молчит."

class Dog(Animal):
    def speak(self):
        return f"{self.name} говорит: Гав!"

class Cat(Animal):
    def speak(self):
        return f"{self.name} говорит: Мяу!"

class Duck(Animal):
    def speak(self):
        return f"{self.name} говорит: Кря!"

animals = [Dog("Бобик"), Cat("Мурка"), Duck("Кряк")]
for animal in animals:
    print(animal.speak())
Бобик говорит: Гав!
Мурка говорит: Мяу!
Кряк говорит: Кря!

Внутри цикла мы не задаём вопрос «а кто ты такой?» — каждое животное само знает, как ему говорить. Если добавится class Cow(Animal): со своим speak(), цикл не изменится ни на строчку.

Иллюстрация: цикл for animal in animals: animal.speak() в центре, от него три стрелки к трём объектам — Dog (выдаёт Гав!), Cat (выдаёт Мяу!), Duck (выдаёт Кря!). Подпись наверху: один вызов, разное поведение

Абстрактные классы

Часто хочется быть уверенным, что все потомки точно реализовали нужный метод. Например, базовый Shape должен заставлять каждый конкретный класс (Rectangle, Circle, …) реализовать area(). Если кто-то забудет, лучше пусть это обнаружится сразу, а не во время работы программы.

Для этого в Python есть абстрактные базовые классы из модуля abc:

Python 3.13
from abc import ABC, abstractmethod
import math

class Shape(ABC):
    @abstractmethod
    def area(self):
        pass

    @abstractmethod
    def perimeter(self):
        pass

    def describe(self):
        # Использует area() и perimeter(), не зная, как они реализованы
        return f"Площадь: {self.area():.2f}, периметр: {self.perimeter():.2f}"

class Rectangle(Shape):
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height

    def perimeter(self):
        return 2 * (self.width + self.height)

class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius

    def area(self):
        return math.pi * self.radius ** 2

    def perimeter(self):
        return 2 * math.pi * self.radius

shapes = [Rectangle(5, 3), Circle(4)]
for i, shape in enumerate(shapes, 1):
    print(f"Фигура {i}: {shape.describe()}")
Фигура 1: Площадь: 15.00, периметр: 16.00
Фигура 2: Площадь: 50.27, периметр: 25.13

Что нам дал ABC:

  • Класс Shape нельзя инстанцировать (Shape() упадёт). Это и логично: «фигура» сама по себе бессмысленна, нужна конкретная.
  • Если бы Circle забыл определить perimeter(), Python отказался бы создавать объект Circle() с ошибкой ещё на этапе создания, а не позже при вызове несуществующего метода.

При этом метод describe() определён прямо в Shape и работает для любого потомка — он опирается на area() и perimeter(), не зная их реализации. Это и есть полиморфизм через абстрактный базовый класс.

Утиная типизация

Python идёт ещё дальше: для полиморфизма не обязательно общее наследование. Если объект ведёт себя как нужно (имеет правильные методы), он сойдёт. Эту идею формулируют так: «если оно ходит как утка и крякает как утка, то это утка».

Python 3.13
class Duck:
    def swim(self):
        return "Утка плывёт."

    def sound(self):
        return "Кря-кря!"

class Person:
    def swim(self):
        return "Человек плывёт."

    def sound(self):
        return "Привет!"

def describe(entity):
    print(entity.swim())
    print(entity.sound())

describe(Duck())
Утка плывёт.
Кря-кря!
describe(Person())
Человек плывёт.
Привет!

Функция describe не проверяет, утка перед ней или человек. Ей важно только одно: чтобы у объекта были методы swim() и sound(). Никакого общего родителя у Duck и Person нет, но полиморфизм работает.

Это очень питоновский подход: вместо «опиши тип» — «опиши поведение». На практике он часто экономит лишние слои абстракций.

Проверка понимания

Что из перечисленного ниже является примером полиморфизма в Python?


Полиморфизм закрывает четыре принципа ООП. Главный практический эффект: код, который вызывает методы через общий интерфейс, не нужно переписывать, когда появляются новые классы. Расширяемость встроена в саму архитектуру.