Полиморфизм в программировании: виды и примеры на Python и C++

Полиморфизм позволяет одному коду работать с объектами разных типов: функция вызывает общий метод, а поведение выбирает сам объект.

Разбираем четыре вида полиморфизма по классификации Стрейчи и Карделли, раннее и позднее связывание, примеры на Python и C++ с настоящим выводом и пять ошибок, о которых спрашивают на собеседованиях.

Статью написал:
Ваня Буявец, продюсер, основатель Checkroi
Ваня Буявец
Основатель Checkroi, продюсер, эксперт в выборе онлайн-курсов
Все 2944 статьи автора Подписаться на Телеграм-канал
Одобрено экспертом:
Наташа Буявец, основатель Checkroi, эксперт по онлайн-курсам
Наташа Буявец
Основательница Checkroi, продюсер Youtube-каналов, эксперт по онлайн-курсам
Все 3598 экспертных мнений Подписаться на Телеграм-канал
Обложка: Полиморфизм в программировании: виды и примеры на Python и C++

Полиморфизм в программировании — это способность одного и того же кода работать с данными разных типов. Вызов один, а что именно произойдёт, решает объект или тип значения. Слово греческое и означает «много форм».

Ниже разбираем виды полиморфизма, раннее и позднее связывание и типичные ошибки. Примеры на Python и C++ мы запустили на Python 3.14 и 3.12, а также на clang 17 и g++ 13: вывод в статье взят из терминала. Если слова «класс» и «метод» пока путаются, начните со статьи «ООП простыми словами», а место полиморфизма среди четырёх принципов показано в разборе принципов ООП.

В биологии и химии полиморфизмом называют другое: разные формы одного вида или кристаллы одного вещества с разной решёткой. Здесь речь только о коде.

CheckroiCheckroiПодборка курсов по ООП171 курс • 33 школыСравните цены, школы, программу и найдите выгодные предложения по обучениюСравнить→

Что такое полиморфизм простыми словами

Рой открывает три разных замка одним ключом

Представьте кнопку «Оплатить» в интернет-магазине. Покупатель выбирает карту, СБП или баланс бонусов, а кнопка одна. Магазину не нужно знать, как каждый способ списывает деньги: он просит «оплати 1500 рублей», и способ оплаты делает это по-своему.

Так же устроен полиморфизм в коде. Функция вызывает общую операцию, а конкретное поведение подставляется в момент вызова.

С полиморфизмом код пишут один раз для общего интерфейса и не трогают, когда появляется новый тип данных.

Если кратко, в объектно-ориентированном программировании это значит: объекты с одинаковым набором методов могут иметь разную реализацию этих методов.

Вы уже пользовались полиморфизмом, даже если не знали термина. Оператор + в Python складывает числа, склеивает строки и объединяет списки, а len() считает длину чего угодно:

print(2 + 3)
print("2" + "3")
print([2] + [3])
print(len("кот"), len([1, 2, 3]), len({"a": 1}))

Вывод:

5
23
[2, 3]
3 3 1

Символ один, поведение зависит от типа. Как работают остальные знаки, разобрано в статье об операторах Python. Запустить примеры у себя поможет инструкция как создать файл Python в VS Code.

Виды полиморфизма

Четыре предмета символизируют четыре вида полиморфизма

Два вида выделил британский учёный Кристофер Стрейчи в лекциях 1967 года для летней школы по программированию в Копенгагене. Текст лекций переиздали в 2000 году в журнале Higher-Order and Symbolic Computation. Это параметрический полиморфизм, когда функция одинаково работает с целым набором типов, и ad hoc, когда для каждого типа может быть своё, никак не связанное поведение.

В 1985 году Лука Карделли и Питер Вегнер уточнили схему в статье «On Understanding Types, Data Abstraction, and Polymorphism» (Computing Surveys, т. 17, № 4). Они добавили третий вид, полиморфизм включения, чтобы описать подтипы и наследование. Получилось четыре разновидности в двух группах:

Вид Группа Как выглядит в коде Пример
Параметрический Универсальный Обобщённые функции и классы с параметром типа Дженерики Python и Java, шаблоны C++
Включения (подтипов) Универсальный Наследник подставляется вместо родителя и переопределяет метод Классы оплаты с общим методом pay()
Перегрузка Ad hoc Несколько функций с одним именем и разными параметрами show(int) и show(string) в C++
Приведение типов Ad hoc Язык сам преобразует значение к нужному типу 1 + 2.5 в Python даёт 3.5, целое стало дробным

Когда на собеседовании спрашивают про полиморфизм в ООП, обычно имеют в виду полиморфизм подтипов. С него и начнём.

CheckroiCheckroiПодборка курсов по Python464 курса • 48 школСравните цены, школы, программу и найдите выгодные предложения по обучениюСравнить→ Канал основателя Checkroi Вани Буявца3 700 человек читают мой Телеграм-канал про нейросетиСобрал промпты для Claude Code и ChatGPT, разборы ИИ-инструментов и лайфхаки по продвижению бизнеса в одном местеПрисоединиться

Подтипы на Python: один метод, разные классы

Задача: выгрузить отчёт о продажах курсов в CSV или JSON. Функция save_report не знает, какой формат ей передали. Она вызывает export(), а каждый класс выгрузки отвечает за свой формат сам.

import json


class Exporter:
    def export(self, rows):
        raise NotImplementedError


class CsvExporter(Exporter):
    def export(self, rows):
        return "\n".join(f"{name};{price}" for name, price in rows)


class JsonExporter(Exporter):
    def export(self, rows):
        return json.dumps(dict(rows), ensure_ascii=False)


def save_report(exporter, rows):
    print(exporter.export(rows))


rows = [("Python", 4900), ("SQL", 3900)]
for exporter in (CsvExporter(), JsonExporter()):
    save_report(exporter, rows)

Вывод:

Python;4900
SQL;3900
{"Python": 4900, "SQL": 3900}

Понадобится выгрузка в Excel, и вы добавите класс XlsxExporter с тем же методом. Функцию save_report трогать не придётся.

Наследовать Exporter в Python, строго говоря, не обязательно. Интерпретатор не проверяет тип, он просто вызывает метод, и этот стиль называют утиной типизацией (определение есть в глоссарии Python). Базовый класс полезен как договор: по нему видно, какой метод обязан быть у каждого формата.

Чем заменить цепочку isinstance

Признак того, что полиморфизма не хватает, выглядит так: функция перебирает типы вручную.

  • if isinstance(x, CsvExporter): ...
  • elif isinstance(x, JsonExporter): ...
  • и ещё десяток веток на каждый новый формат.

Каждый новый тип заставляет править эту функцию. Решение: перенести ветку в метод класса, как в примере выше. Это прямое применение принципа открытости и закрытости из SOLID: код открыт для новых типов и закрыт для правок.

Выбор по типу и дженерики в Python

Перегрузки методов в Python нет: второе определение с тем же именем затирает первое (пример ошибки ниже). Но разное поведение для разных типов получить можно.

singledispatch: своя функция под каждый тип

Декоратор @singledispatch из модуля functools превращает функцию в обобщённую. Реализация выбирается по типу первого аргумента, так написано в документации functools. Декоратор появился в Python 3.4, а вариант для методов класса singledispatchmethod добавили в 3.8.

from functools import singledispatch


@singledispatch
def describe(value):
    return f"неизвестный тип: {type(value).__name__}"


@describe.register
def _(value: int):
    return f"целое число, {value.bit_length()} бит"


@describe.register
def _(value: list):
    return f"список из {len(value)} элементов"


print(describe(255))
print(describe([1, 2, 3]))
print(describe(3.14))

Вывод:

целое число, 8 бит
список из 3 элементов
неизвестный тип: float

Для float отдельной версии нет, поэтому сработала базовая функция. Это и есть ad hoc полиморфизм в чистом виде: на каждый тип своя реализация.

Дженерики: одна функция для любого типа

Параметрический полиморфизм в Python записывается через параметр типа в квадратных скобках. Такой синтаксис появился в Python 3.12:

def first[T](items: list[T]) -> T:
    return items[0]


print(first([10, 20, 30]))
print(first(["кот", "пёс"]))

Вывод:

10
кот

Код внутри first один и тот же для чисел и строк. Учтите, что интерпретатор аннотации не проверяет: T нужен анализаторам типов и редактору кода, а на выполнение не влияет. В Java и C++ параметр типа проверяет компилятор.

Справка по модулям functools и typing лежит на официальном сайте языка. Где там что искать, показано в статье о python.org и документации Python.

CheckroiCheckroiПодборка курсов по C++62 курса • 19 школСравните цены, школы, программу и найдите выгодные предложения по обучениюСравнить→

Раннее и позднее связывание на примере C++

Чтобы полиморфизм подтипов работал, программа должна выбрать нужную версию метода во время выполнения, а не при компиляции. Это называют поздним (динамическим) связыванием. Если версия выбирается по типу переменной ещё при сборке, связывание раннее.

В C++ разницу видно лучше всего: метод становится полиморфным, только если объявлен virtual. Ниже у pay() слово virtual есть, а у kind() нет.

#include <iostream>
#include <memory>
#include <string>
#include <vector>

struct Payment {
    virtual std::string pay(int rub) const { return "оплата " + std::to_string(rub) + " ₽"; }
    std::string kind() const { return "платёж"; }
    virtual ~Payment() = default;
};

struct Card : Payment {
    std::string pay(int rub) const override { return "карта: " + std::to_string(rub) + " ₽"; }
    std::string kind() const { return "карта"; }
};

struct Sbp : Payment {
    std::string pay(int rub) const override { return "СБП: " + std::to_string(rub) + " ₽"; }
    std::string kind() const { return "СБП"; }
};

int main() {
    std::vector<std::unique_ptr<Payment>> cart;
    cart.push_back(std::make_unique<Card>());
    cart.push_back(std::make_unique<Sbp>());
    for (const auto& p : cart) {
        std::cout << p->kind() << " | " << p->pay(1500) << "\n";
    }
}

Вывод:

платёж | карта: 1500 ₽
платёж | СБП: 1500 ₽

Указатель имеет тип Payment, а объекты внутри разные. Метод pay() вызвался у настоящего объекта, карты или СБП. Метод kind() компилятор привязал к типу указателя, поэтому оба раза вышло «платёж», хотя в наследниках он переопределён. Компиляторы обычно реализуют позднее связывание через таблицу виртуальных функций: у каждого такого класса своя таблица, а объект хранит указатель на неё.

В разных языках умолчания разные, и на этом часто ловят новичков:

Язык Методы полиморфны по умолчанию Что важно знать
Python Да Метод ищется у объекта при каждом вызове
Java Да, у методов экземпляра Статический метод наследника не переопределяет, а скрывает метод родителя (руководство Oracle)
C# Нет Нужны virtual у родителя и override у наследника (справочник C#)
C++ Нет Без virtual метод связывается рано, как kind() выше

В Java механизм называется вызовом виртуального метода: JVM вызывает метод того объекта, на который указывает переменная, а не того типа, которым переменная объявлена. Так он описан в разделе о полиморфизме руководства Oracle. Кто выбирает между JVM-языками, найдёт сравнение в статье Java или Kotlin.

Перегрузка и шаблоны в C++

Две другие формы в C++ работают при компиляции. Перегрузка выбирает функцию по типам аргументов, шаблон генерирует версию функции под каждый тип:

#include <iostream>
#include <string>

void show(int x) { std::cout << "int: " << x << "\n"; }
void show(double x) { std::cout << "double: " << x << "\n"; }
void show(const std::string& x) { std::cout << "строка: " << x << "\n"; }

template <typename T>
T max_of(T a, T b) { return a > b ? a : b; }

int main() {
    show(42);
    show(2.5);
    show(std::string("кот"));
    std::cout << max_of(3, 7) << " " << max_of(2.5, 1.5) << "\n";
}

Вывод:

int: 42
double: 2.5
строка: кот
7 2.5

Если вы идёте в C++ с чистого Си, где классов и перегрузки нет, разницу двух профессий объясняет статья кто такой разработчик на Си.

Частые ошибки и что с ними делать

Рой нашёл ошибку в коде и радуется

Ниже пять ошибок, которые встречаются в коде новичков и в вопросах на собеседованиях.

1. Две версии метода с одним именем в Python. Кажется, что это перегрузка, но второе определение просто заменяет первое:

class Report:
    def render(self, title):
        return f"<h1>{title}</h1>"

    def render(self, title, footer):
        return f"<h1>{title}</h1><p>{footer}</p>"


print(Report().render("Итоги"))

Вывод:

TypeError: Report.render() missing 1 required positional argument: 'footer'

Решение: один метод с аргументом по умолчанию (footer=None) или singledispatchmethod, если ветки различаются по типу.

2. Срезка объекта в C++. Наследника передали в функцию по значению, и от него осталась только родительская часть. 3. Вызов виртуального метода из конструктора. Пока работает конструктор родителя, наследника ещё нет. Обе ошибки видны в одной программе:

#include <iostream>
#include <string>

struct Payment {
    Payment() { std::cout << "конструктор: " << name() << "\n"; }
    virtual std::string name() const { return "платёж"; }
    virtual ~Payment() = default;
};

struct Card : Payment {
    std::string name() const override { return "карта"; }
};

void by_value(Payment p) { std::cout << "по значению: " << p.name() << "\n"; }
void by_ref(const Payment& p) { std::cout << "по ссылке: " << p.name() << "\n"; }

int main() {
    Card card;
    by_value(card);
    by_ref(card);
}

Вывод:

конструктор: платёж
по значению: платёж
по ссылке: карта

Конструктор напечатал «платёж», хотя создавалась карта: справочник cppreference прямо говорит, что во время конструирования вызывается версия класса, чей конструктор сейчас работает. Функция by_value тоже напечатала «платёж», потому что получила копию родителя. Решение: передавать объекты по ссылке или указателю, а в конструкторе виртуальные методы не вызывать.

4. Опечатка в сигнатуре. В наследнике написали pay(long) вместо pay(int), и получился новый метод вместо переопределения. Спасает слово override: компилятор проверит, что метод переопределяет родительский, а не создаёт новый. Clang 17 на таком коде пишет:

error: non-virtual member function marked 'override' hides virtual member function

В Java ту же проверку делает аннотация @Override.

5. Ручной перебор типов. Цепочки isinstance в Python или instanceof в Java на каждый новый класс. Решение то же, что в разделе про подтипы: перенести поведение в метод.

Ваня БуявецКанал основателя Checkroi Вани БуявцаЗабирайте промпты и обучение по нейросетям в моём Телеграм-каналеБольше 3 700 человек уже применяют Claude Code, ChatGPT и другие нейросети в работе, учёбе, бизнесе и жизниПерейти в канал

Как ответить на собеседовании

Вопрос «что такое полиморфизм» входит в базовые на собеседовании джуна. Хороший ответ укладывается в минуту:

  1. Определение одной фразой: один интерфейс, разные реализации, выбор по типу объекта.
  2. Виды: подтипов, параметрический и ad hoc (перегрузка, приведение типов).
  3. Пример из своего кода, хотя бы как с выгрузкой отчёта выше.
  4. Отличие от наследования: наследование передаёт код, полиморфизм позволяет подставить наследника туда, где ждут родителя.
  5. Разница переопределения и перегрузки: первое выбирается при выполнении, второе при компиляции.

Остальные этапы и типичные провалы на интервью собраны в статье как пройти собеседование разработчику.

Где освоить ООП системно

Разобраться в полиморфизме по примерам можно за вечер. Уверенность приходит позже, на своём проекте с десятком классов и код-ревью. Удобнее всего это делать на курсе, где ООП разбирают на практике и проверяют домашние задания. Если пока выбираете язык, посмотрите путь в Python-разработку и в C++, а уровень оплаты на старте показан в обзоре зарплат Python-разработчиков.

Ниже курсы, в программе которых есть ООП:

КурсШколаСтоимость со скидкойВ рассрочкуДлитель­ностьОбзор курса от Checkroi
Профессия «Python-разработчик»
Перейти на сайт курса
SkillboxSkillbox157 335 ₽5987 ₽/мес.10 месяцевОбзор курса
PHP-разработчик с нуля до PRO
Перейти на сайт курса
SkillboxSkillbox114 876 ₽4176 ₽/мес.7 месяцевОбзор курса
Frontend-разработчик с нуля
Перейти на сайт курса
НетологияНетология113 400 ₽5385 ₽/мес.10 месяцевОбзор курса
Fullstack-разработчик на Python
Перейти на сайт курса
НетологияНетология161 200 ₽7125 ₽/мес.21 месяцОбзор курса
PHP-разработчик. Базовый уровень
Перейти на сайт курса
SkillboxSkillbox81 107 ₽5227 ₽/мес.3 месяцаОбзор курса

Больше программ — в полном каталоге курсов по оОП

Часто задаваемые вопросы

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

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

Какие виды полиморфизма бывают?

По классификации Луки Карделли и Питера Вегнера 1985 года их четыре. Параметрический полиморфизм и полиморфизм включения (подтипов) относятся к универсальному, перегрузка и приведение типов к ad hoc. Параметрический и ad hoc полиморфизм раньше выделил Кристофер Стрейчи в лекциях 1967 года.

Есть ли перегрузка методов в Python?

Нет. Второе определение метода с тем же именем заменяет первое, и вызов со старым набором аргументов падает с TypeError. Похожего эффекта добиваются аргументами по умолчанию или декоратором functools.singledispatch, который выбирает реализацию по типу первого аргумента.

Что такое позднее связывание?

Это выбор версии метода во время выполнения программы по настоящему типу объекта, а не по типу переменной. В Python и у методов экземпляра в Java оно работает по умолчанию, в C++ и C# метод нужно объявить с ключевым словом virtual.

Полиморфизм бывает только в ООП?

Нет. Параметрический полиморфизм, то есть дженерики и шаблоны, работает и в коде без классов: функция first из примера на Python обычная, классов в ней нет. Ad hoc полиморфизм тоже не требует ООП, оператор + в Python складывает числа и склеивает строки без всяких классов.

Читайте Checkroi первым в Google
Добавьте Checkroi в избранные источники — и наши разборы курсов и обзоры школ будут показываться выше в вашей выдаче Google.
Оставить комментарий
0 комментариев
Форма комментария

Оставьте комментарий

Напишите, что думаете. Нам важно ваше мнение!