Каковы плюсы и минусы использования абстрактных классов (ABC) для создания интерфейсов в Python?

Ответ

Абстрактные базовые классы (Abstract Base Classes, ABC) из модуля abc позволяют определять "контракты" для дочерних классов, обязывая их реализовывать определенные методы.

Преимущества

  • Гарантия реализации контракта: С помощью декоратора @abstractmethod можно указать, какие методы обязательны для реализации в подклассах. Попытка создать экземпляр дочернего класса без реализации этих методов вызовет TypeError.
  • Явное наследование: Иерархия классов становится очевидной и легко отслеживаемой. Проверка isinstance() и issubclass() работает ожидаемо.
  • Запрет на создание экземпляров: Нельзя создать объект самого абстрактного класса, что логично, так как он представляет собой неполную реализацию.

Недостатки

  • Жесткая иерархия наследования: Класс должен явно наследоваться от ABC, что создает сильную связь. Это может быть неудобно, если класс уже является частью другой иерархии или если вы не можете изменять его исходный код.
  • Более многословный синтаксис: Требуется импортировать ABC и abstractmethod, а также явно указывать метакласс.

Пример

from abc import ABC, abstractmethod

# Абстрактный класс определяет интерфейс "Хранилище"
class BaseStorage(ABC):
    @abstractmethod
    def save(self, data: dict):
        """Сохраняет данные."""
        pass

    @abstractmethod
    def load(self, item_id: str) -> dict:
        """Загружает данные."""
        pass

# Конкретная реализация интерфейса
class FileStorage(BaseStorage):
    def save(self, data: dict):
        print(f"Сохранение {data} в файл...")

    def load(self, item_id: str) -> dict:
        print(f"Загрузка {item_id} из файла...")
        return {"id": item_id, "data": "some_data"}

# Попытка создать экземпляр без реализации метода вызовет ошибку
class IncompleteStorage(BaseStorage):
    def save(self, data: dict):
        pass
    # Метод load() не реализован

# fs = IncompleteStorage()  # TypeError: Can't instantiate abstract class ...

fs = FileStorage()
fs.save({"id": "123"})

Альтернатива: Protocol

В современном Python для определения интерфейсов часто предпочитают typing.Protocol. Он реализует структурную типизацию (duck typing), проверяя наличие нужных методов без явного наследования, что обеспечивает большую гибкость.

Ответ 18+ 🔞

Так, слушай, смотри, вот есть в Питоне такая штука — абстрактные базовые классы, они же ABC. Из модуля abc, да. Ну, это типа такие заготовки, контракты, которые ты пишешь и говоришь: «Вот, блядь, все, кто от меня наследуется, обязаны реализовать вот эти методы, а иначе — пиздец, ошибка».

Что в этом хорошего, ёпта?

  • Контракт, блядь, железный: Вешаешь декоратор @abstractmethod на метод — и всё, при попытке создать объект дочернего класса без этого метода вылетит TypeError. Никаких «ой, забыл». Не реализовал — сиди, мудак, исправляй.
  • Всё прозрачно, как слёзы ребёнка: Иерархия классов видна невооружённым глазом. isinstance() и issubclass() работают как часы, без сюрпризов.
  • Защита от дурака: Сам абстрактный класс инстанс создать нельзя. Ну логично же, зачем тебе полуфабрикат? Это просто план, чертёж, а не дом.

А что плохого, хитрая жопа?

  • Жёсткая привязка, блядь: Класс должен прямо, в лоб, наследоваться от твоего ABC. А если он уже в другой иерархии сидит? Или код чужой, и ты его не трогаешь? Вот тут и начинается песня.
  • Писать больше надо: Надо импортить, метаклассы указывать... В общем, мороки не на один чих.

Смотри, как это выглядит на практике:

from abc import ABC, abstractmethod

# Вот наш главный по тарелкам — абстрактное «Хранилище»
class BaseStorage(ABC):
    @abstractmethod
    def save(self, data: dict):
        """Сохраняет данные."""
        pass

    @abstractmethod
    def load(self, item_id: str) -> dict:
        """Загружает данные."""
        pass

# А вот конкретный работяга, который всё делает
class FileStorage(BaseStorage):
    def save(self, data: dict):
        print(f"Сохранение {data} в файл...")

    def load(self, item_id: str) -> dict:
        print(f"Загрузка {item_id} из файла...")
        return {"id": item_id, "data": "some_data"}

# А вот ленивый распиздяй, который метод не дописал
class IncompleteStorage(BaseStorage):
    def save(self, data: dict):
        pass
    # Метод load() забыл, сука!

# storage = IncompleteStorage()  # Тут тебе и выедет: TypeError: Can't instantiate abstract class ... Пиздец и привет.

# А этот работает
fs = FileStorage()
fs.save({"id": "123"})

P.S. Вообще, сейчас модно использовать typing.Protocol для таких дел. Это структурная типизация, утиное типирование, ёпта. Проверяет не «кто твой папа» (наследование), а «что ты умеешь» (наличие методов). Гибче, блядь, в разы. Но это уже другая история, про которую можно овердохуища рассказать.