Полиморфизм функций в Python: один интерфейс — разные объекты
💻 Информатика · 10 класс
Полиморфизм в Python держится на утиной типизации: функции важно, что объект умеет, а не от какого он класса. Разбираем общий интерфейс вызова, отличие перегрузки операторов от перегрузки функций.

Один интерфейс — разные объекты
Полиморфизм буквально означает «много форм»: одна и та же операция работает с объектами разных типов. Встроенная функция len() — готовый пример: она считает символы строки, элементы списка и пары словаря, не спрашивая заранее, что ей передали.
print(len('кот')) # 3 — символы строки
print(len([1, 2, 3])) # 3 — элементы списка
print(len({'a': 1, 'b': 2})) # 2 — пары словаряМы не выбирали версию len() в зависимости от типа — Python сам нашёл нужное поведение у каждого объекта.
Утиная типизация
Официальный глоссарий Python называет такой стиль утиной типизацией: тип объекта не проверяется, нужный метод или атрибут вызывается напрямую. «Если это выглядит как утка и крякает как утка, значит, это утка». Интерфейс важнее происхождения класса: функции достаточно, чтобы у каждого переданного объекта был метод area().
Пример 1. Суммарная площадь фигур
import math
class Circle:
def __init__(self, radius):
if radius <= 0:
raise ValueError('радиус должен быть положительным')
self.radius = radius
def area(self):
return math.pi * self.radius ** 2
class Square:
def __init__(self, side):
if side <= 0:
raise ValueError('сторона должна быть положительной')
self.side = side
def area(self):
return self.side ** 2
def total_area(shapes):
return sum(shape.area() for shape in shapes)
figures = [Circle(1), Square(2), Circle(0.5)]
print(total_area(figures)) # сумма трёх площадейКлассы Circle и Square не связаны наследованием и не знают друг о друге. Но оба отвечают на вызов area(), поэтому total_area принимает любую смесь фигур. Проверено выполнением с точностью до 1e-9, в том числе для total_area([]): сумма по пустому списку равна 0. В учебном примере размеры предполагаются конечными числами. Инициализаторы отклоняют нулевые и отрицательные размеры через ValueError: вызов класса не возвращает успешно инициализированный экземпляр.
Пример 2. Когда интерфейса нет
Утиная типизация честно сообщает о проблеме: если у объекта нет нужного метода, возникнет AttributeError.
class Stone:
pass
try:
total_area([Stone()])
except AttributeError:
print('объект не умеет вычислять площадь')Перехватывается именно AttributeError — конкретное исключение, а не всё подряд: но такой обработчик поймает и AttributeError, возникший внутри area(), например из-за опечатки в имени атрибута. Поэтому он годится здесь для демонстрации, а причину неожиданной ошибки нужно искать по traceback, не объявляя любой AttributeError отсутствием метода.
Перегрузка операторов — не перегрузка функций
Не путайте два понятия. Перегрузка операторов — когда класс задаёт поведение оператора для своих объектов, например +:
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
if not isinstance(other, Vector):
return NotImplemented
return Vector(self.x + other.x, self.y + other.y)
v = Vector(1, 2) + Vector(3, 4)
print(v.x, v.y) # 4 6Метод __add__ объясняет, что означает + для Vector; для неподдерживаемого типа он возвращает NotImplemented, позволяя Python попробовать отражённую операцию или сообщить TypeError. Обычные определения def с одним именем не создают перегрузку по числу или типам аргументов: второй def привязывает имя к новой функции. Старую функцию можно сохранить заранее под другим именем. Проверено: после повторного определения вызов со «старым» числом аргументов выбрасывает TypeError. Функцию с гибким числом аргументов собирают иначе — значениями по умолчанию или *args.
В рассмотренных примерах полиморфизм означает одну операцию, работающую с разными объектами: сложение склеивает строки и складывает числа, а смешать их ('2' + 3) нельзя — будет TypeError. Оба случая проверены выполнением. Для выбора реализации по типу первого аргумента существует functools.singledispatch; typing.overload описывает варианты вызова для статического анализатора и сам не выбирает реализацию при запуске.
Проверь себя
- Почему total_area работает со списком из Circle и Square? Ответ: у обоих классов есть метод area(), а тип объектов функция не проверяет.
- Что будет, если передать в total_area объект без метода area()? Ответ: AttributeError при попытке вызова.
- Нужно ли Circle и Square наследовать от общего класса, чтобы пример 1 работал? Ответ: нет — достаточно общего интерфейса вызова.
- Можно ли в Python объявить две функции size с разным числом аргументов? Ответ: два def написать можно, но второе определение привяжет имя к новой функции; автоматического выбора по числу аргументов не получится. Для гибкости используют значения по умолчанию или *args.
- Что вернёт len для объекта собственного класса? Ответ: если класс реализует __len__, len вернёт вычисленную длину — неотрицательное целое. Без поддержки длины возникнет TypeError; некорректный результат __len__ тоже вызовет исключение.
Кратко о главном
Полиморфизм — один интерфейс вызова у разных объектов. Python опирается на утиную типизацию: важно, что объект умеет, а не от какого класса произведён. Перегрузка операторов (методы вида __add__) задаёт поведение операторов для класса; повторные def не создают автоматический выбор функции по сигнатуре. Отсутствие метода — законный AttributeError, а не повод перехватывать все исключения подряд.