W3docs

Полиморфизм в Python

Полиморфизм в Python: переопределение методов, утиная типизация, перегрузка операторов и абстрактные интерфейсы — с понятными примерами.

Полиморфизм (от греческого «многообразие форм») позволяет одному фрагменту кода работать с объектами разных типов, если эти объекты поддерживают ожидаемый интерфейс. Вы вызываете одно и то же имя метода для каждого объекта, и каждый объект отвечает в соответствии со своей реализацией.

Полиморфизм — одна из четырёх основ объектно-ориентированного программирования наряду с инкапсуляцией, наследованием и абстракцией. Именно он позволяет функциям, принимающим тип базового класса, автоматически работать с любым подклассом.

В этой главе рассматривается:

  • Что такое полиморфизм и почему он важен
  • Полиморфизм через переопределение методов
  • Полиморфизм через утиную типизацию
  • Перегрузка операторов — форма полиморфизма, встроенная в Python
  • Абстрактные базовые классы как формальный способ определения полиморфных интерфейсов
  • Практические паттерны и типичные ошибки

Прежде чем читать эту главу, убедитесь, что вы разобрались с классами и объектами Python и наследованием в Python.

Зачем нужен полиморфизм

Без полиморфизма функция, работающая с животными, потребовала бы явной цепочки if/elif для каждого типа животного:

def make_sound(animal):
    if type(animal).__name__ == "Dog":
        print("Woof!")
    elif type(animal).__name__ == "Cat":
        print("Meow!")
    elif type(animal).__name__ == "Bird":
        print("Tweet!")
    # ... add a new branch every time you add a new animal type

Такой подход ненадёжен. Каждый новый тип животного требует изменения этой функции. С полиморфизмом код становится таким:

def make_sound(animal):
    animal.speak()   # works for any object that has a speak() method

Добавление нового типа животного требует только определения его метода speak() — сама функция никогда не меняется. Это и есть принцип открытости/закрытости: открыто для расширения, закрыто для изменения.

Полиморфизм через переопределение методов

Наиболее распространённая форма полиморфизма в Python — переопределение методов: подкласс предоставляет собственную версию метода, определённого в родительском классе.

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

    def speak(self):
        return f"{self.name} makes a sound."


class Dog(Animal):
    def speak(self):
        return f"{self.name} says woof!"


class Cat(Animal):
    def speak(self):
        return f"{self.name} says meow!"


class Bird(Animal):
    def speak(self):
        return f"{self.name} says tweet!"

Теперь один цикл работает со всеми тремя типами:

animals = [Dog("Rex"), Cat("Whiskers"), Bird("Tweety")]

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

# Rex says woof!
# Whiskers says meow!
# Tweety says tweet!

animal.speak() во время выполнения выбирает нужную версию в зависимости от фактического типа объекта. Этот выбор во время выполнения называется динамической диспетчеризацией или поздним связыванием.

Расширение versus замена метода родителя

При переопределении вы можете либо полностью заменить поведение родителя, либо расширить его с помощью super():

class Animal:
    def speak(self):
        print("[Animal vocalization]")


class Dog(Animal):
    def speak(self):
        super().speak()           # keep the parent's output
        print("Woof! (Dog override adds this)")


Dog().speak()
# [Animal vocalization]
# Woof! (Dog override adds this)

Используйте super(), когда версия родителя выполняет полезную настройку или логирование, которые должны сохраниться. Опускайте его, когда хотите полностью заменить поведение.

Полиморфизм через утиную типизацию

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

class Dog:
    def speak(self):
        return "Woof!"


class Robot:
    def speak(self):
        return "Beep boop."


class Human:
    def speak(self):
        return "Hello!"


def introduce(entity):
    print(entity.speak())


introduce(Dog())    # Woof!
introduce(Robot())  # Beep boop.
introduce(Human())  # Hello!

Dog, Robot и Human не имеют общего родительского класса (кроме встроенного object). Тем не менее introduce() работает со всеми тремя, поскольку у каждого есть метод speak(). Функция не проверяет тип entity — она просто вызывает метод и доверяет, что объект ответит корректно.

Когда использовать утиную типизацию, а когда — наследование

СитуацияПредпочтительный подход
Объекты логически связаны (все — животные)Иерархия наследования
Объекты не связаны, но разделяют поведениеУтиная типизация
Нужно принудительно проверять интерфейс при определенииАбстрактные базовые классы
Работа со встроенными типами или сторонними классами, которые нельзя изменитьУтиная типизация

Утиная типизация идиоматична для Python и широко используется в стандартной библиотеке — например, len() работает с любым объектом, у которого определён __len__, независимо от его класса.

Полиморфизм с функциями и циклами

Вы можете написать одну функцию, которая единообразно обращается с разными типами через полиморфизм. Рассмотрим приложение для рисования:

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

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

    def describe(self):
        return f"Circle with radius {self.radius}"


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

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

    def describe(self):
        return f"Rectangle {self.width}x{self.height}"


class Triangle:
    def __init__(self, base, height):
        self.base = base
        self.height = height

    def area(self):
        return 0.5 * self.base * self.height

    def describe(self):
        return f"Triangle base={self.base} height={self.height}"


shapes = [Circle(5), Rectangle(4, 6), Triangle(3, 8)]

for shape in shapes:
    print(f"{shape.describe()}: area = {shape.area():.2f}")

# Circle with radius 5: area = 78.54
# Rectangle 4x6: area = 24.00
# Triangle base=3 height=8: area = 12.00

Цикл вызывает area() и describe() для каждой фигуры, не заботясь о том, какому классу принадлежит объект. Добавление класса Pentagon позже потребует только написания нового класса — цикл не изменится.

Перегрузка операторов

Арифметические операторы и операторы сравнения в Python тоже полиморфны. + для целых чисел складывает их; + для строк конкатенирует их; + для списков объединяет их. Python реализует это через специальные (dunder) методы.

Вы можете заставить собственные классы реагировать на операторы, определив эти методы:

class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __add__(self, other):
        return Vector(self.x + other.x, self.y + other.y)

    def __mul__(self, scalar):
        return Vector(self.x * scalar, self.y * scalar)

    def __repr__(self):
        return f"Vector({self.x}, {self.y})"


v1 = Vector(1, 2)
v2 = Vector(3, 4)

print(v1 + v2)    # Vector(4, 6)
print(v1 * 3)     # Vector(3, 6)

Один и тот же оператор + теперь ведёт себя по-разному в зависимости от того, являются ли операнды целыми числами, строками или объектами Vector. Это полиморфизм на уровне операторов.

Подробнее о специальных методах Python читайте в разделе магические методы Python.

Полиморфизм с абстрактными базовыми классами

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

from abc import ABC, abstractmethod


class Shape(ABC):
    @abstractmethod
    def area(self) -> float:
        """Return the area of the shape."""

    @abstractmethod
    def perimeter(self) -> float:
        """Return the perimeter of the shape."""


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

    def area(self) -> float:
        import math
        return math.pi * self.radius ** 2

    def perimeter(self) -> float:
        import math
        return 2 * math.pi * self.radius


class Square(Shape):
    def __init__(self, side: float):
        self.side = side

    def area(self) -> float:
        return self.side ** 2

    def perimeter(self) -> float:
        return 4 * self.side


def print_info(shape: Shape) -> None:
    print(f"Area:      {shape.area():.2f}")
    print(f"Perimeter: {shape.perimeter():.2f}")


print_info(Circle(5))
# Area:      78.54
# Perimeter: 31.42

print_info(Square(4))
# Area:      16.00
# Perimeter: 16.00

Если попытаться создать экземпляр подкласса, не реализовавшего area():

class Blob(Shape):
    pass   # forgot to implement area() and perimeter()

b = Blob()
# TypeError: Can't instantiate abstract class Blob with abstract methods area, perimeter

ABC дают вам страховочную сеть формального контракта, сохраняя при этом полиморфизм во время выполнения.

ABC против утиной типизации — что выбрать?

  • Утиная типизация проще и гибче. Предпочитайте её для небольших внутренних кодовых баз и скриптов.
  • ABC делают ожидаемый интерфейс явным, рано выявляют ошибки отсутствующих методов и отображаются в автодополнении IDE и инструментах проверки типов. Предпочитайте их в крупных кодовых базах, публичных библиотеках и везде, где важно соблюдение контракта.

Реальный пример: обработка платежей

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

from abc import ABC, abstractmethod


class PaymentProvider(ABC):
    @abstractmethod
    def charge(self, amount: float, currency: str) -> bool:
        """Attempt to charge the given amount. Return True on success."""

    @abstractmethod
    def refund(self, transaction_id: str) -> bool:
        """Refund a previous transaction. Return True on success."""


class StripeProvider(PaymentProvider):
    def charge(self, amount: float, currency: str) -> bool:
        print(f"[Stripe] Charged {amount} {currency}")
        return True

    def refund(self, transaction_id: str) -> bool:
        print(f"[Stripe] Refunded transaction {transaction_id}")
        return True


class PayPalProvider(PaymentProvider):
    def charge(self, amount: float, currency: str) -> bool:
        print(f"[PayPal] Charged {amount} {currency}")
        return True

    def refund(self, transaction_id: str) -> bool:
        print(f"[PayPal] Refunded transaction {transaction_id}")
        return True


def process_order(provider: PaymentProvider, amount: float) -> None:
    success = provider.charge(amount, "USD")
    if success:
        print("Order complete.")


process_order(StripeProvider(), 99.99)
# [Stripe] Charged 99.99 USD
# Order complete.

process_order(PayPalProvider(), 49.5)
# [PayPal] Charged 49.5 USD
# Order complete.

process_order не знает и не заботится о том, получает ли он StripeProvider или PayPalProvider. Добавление нового CryptoProvider требует только написания нового класса — больше ничего не меняется. Это полиморфизм, обеспечивающий реальную расширяемость.

Типичные ошибки

Ошибка 1: использование type() вместо isinstance()

Сравнение type(obj) == Dog нарушает полиморфизм, потому что для подклассов возвращает False. Предпочитайте isinstance(obj, Animal), который возвращает True как для Dog, так и для любого будущего подкласса:

class Animal:
    pass

class Dog(Animal):
    pass

d = Dog()

# Fragile — breaks for subclasses:
print(type(d) == Animal)   # False

# Correct — subclass-aware:
print(isinstance(d, Animal))  # True

Ошибка 2: несовместимые сигнатуры методов

Полиморфизм предполагает, что все реализации метода принимают одинаковые аргументы. Если Dog.speak() требует аргумент, которого нет у Cat.speak(), вызывающий код, обращающийся к ним единообразно, сломается:

# Inconsistent — will cause errors in a loop
class Dog:
    def speak(self, volume):   # extra argument!
        return f"Woof at volume {volume}"

class Cat:
    def speak(self):
        return "Meow!"

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

Ошибка 3: изменяемые аргументы по умолчанию в переопределённых методах

Это более широкая ловушка Python, но она особенно часто встречается в иерархиях классов: никогда не используйте изменяемое значение по умолчанию (список, словарь) в качестве аргумента по умолчанию — оно создаётся один раз и разделяется между всеми вызовами.

# Bug: the list is shared across all instances
class Item:
    def __init__(self, tags=[]):   # BAD
        self.tags = tags

# Fix:
class Item:
    def __init__(self, tags=None):
        self.tags = tags if tags is not None else []

Итоги

КонцепцияЧто означает
Переопределение методаПодкласс предоставляет собственную версию метода родителя
Динамическая диспетчеризацияPython выбирает нужную версию метода во время выполнения
Утиная типизацияПодходит любой объект с нужными методами, независимо от его класса
Перегрузка операторовDunder-методы (__add__, __len__, …) делают операторы полиморфными
Абстрактные базовые классыФормально обязывают подклассы реализовывать нужные методы

Полиморфизм делает код расширяемым без изменений. Пишите функции, которые зависят от поведения (имён методов), а не от конкретных типов, — и ваш код будет естественно принимать новые классы без каких-либо изменений.

Практика

Практика
Which of the following statements about Python polymorphism are correct?
Which of the following statements about Python polymorphism are correct?
Was this page helpful?