Полиморфизм бывает разный:
Полиморфизм подтипов - этот тип как раз таки и подразумевают в контексте ООП. Суть в том, что сущность некоторого класса так же может представляться базовым классом или интерфейсом. Это позволяет переиспользовать код, отдавая в один и тот же метод/функцию сущности с разными классами, но с общим интерфейсом.
Параметрический полиморфизм - используется уже не только в ООП, но и в других парадигмах. Опять таки полиморфная функция принимает аргумент разных типов, при этом сам тип так же передается как параметр функции, а следовательно функция может оперировать составными типами на основе переданного. Или например возвращать результат того же или производного типа, сохраняя тем самым тип для вызывающего кода. Чаще всего представлено дженериками, но могут быть и другие формы (например template в C++). Как правило не имеет смысла в языках с динамической типизацией. А еще часто сопровождается контрактами на получаемый тип (например типами высшего порядка или типажами), что позволяет с одной стороны ограничить возможные типы, а с другой - воспользоваться характеристиками типа обусловленными контрактом.
ad-hoc полиморфизм - способность функции предоставить разную реализацию в зависимости от запрашиваемой сигнатуры. Чаще всего выражено перегрузкой функций/методов. Как правило не реализуем средствами языка с динамической типизацией, хотя может быть реализован в рантайме (например в js функция всегда принимает произвольное количество аргументов и может проанализировать их в рантайме с помощью arguments).
В общем случае полиморфизм нужен для переиспользования кода. Соблюдения практик SOLID и DRY не возможно без полиморфизма. Например в языке Go отсутствие полиморфизма в любом виде приводит к загрязнению кодовой базы и большому количеству копипасты.
И еще, из моего ответа, может сложится мнение, что полиморфизм не применим к языкам с динамической типизацией, например к Python. На самом деле это не так, в ЯП с динамической типизацией полиморфизм наоборот возведен в абсолют.
UPD: Примеры:
правда я не шибко хорошо знаю конкретно Python, поэтому абстрактно напишу псевдокод:
Полиморфизм подтипов, как уже говорил, это про ООП. Допустим у меня задача реализовать класс, который читает бинарные данные, проверяет, что они валидный utf-8 и выдает прочитанное как строку. Откуда он читает их? Да мне все равно, это не относятся к моей задаче. Конкретную реализацию я приму в аргументах, а уж откуда она будет читать, из файла или из сети - мне все равно. Главное чтоб эта реализация умела читать байты.
// ok мне нужно что-то, что умеет читать байты, опишу это интерфейсом
interface IByteReader {
readBytes(): BytesIterator;
}
// и мой метод:
readAsString(reader: IByteReader): string {
var iterator: BytesIterator = reader.readBytes();
// ну и тут как то с ним работаю, трансформируя байты в строку
}
теперь вызывающий код может передать в мой метод инстанс любого класса, реализующего мой интерфейс, и мне теперь не нужно дублировать логику чтения строки для файла и для сети.
Еще один яркий пример тут - функция len() в Python, которая принимает любой тип реализующий одноименный магический метод.
Параметрический полиморфизм, это про дженерики. Напишем 2 функции, обе будут принимать на вход массив, одна из них будет возвращать начальный элемент, а другая конечный. Но массивы бывают для элементов разных типов. Без параметрического полиморфизма, нам бы пришлось писать реализацию для каждого нужного типа, но благодаря ему мы можем это сделать в обобщенном виде:
// T - это параметр типа
fitst<T>(arr: [T]): Option<T> {
if(arr.has(0)) return Some(arr[0]);
else return None;
}
// давайте предположим, что некоторые виды массивов у нас могут быть бесконечными
// тогда нам понадобится контракт, что можно передавать массивы только с известным размером
last<T>(arr: [T]): Option<T>
where [T]: WithKnownSize
{
var length: size = get_length(arr);
if(length == 0) return None;
else return arr[length - 1];
}
ad-hoc полиморфизм, это про перегрузку функций, пусть мы хотим функцию print которая умеет печатать числа и строки:
print(value: string) {
stdout.write(value);
}
print(value: int) {
stdout.write(value.to_string());
}