$ sudo teach IT
МОДУЛЬ 4 · УРОК 3

Абстрактные классы и ABC

В прошлых уроках мы узнали про полиморфизм и утиную типизацию. Они дают гибкость — но никто не гарантирует, что разработчик реализует нужные методы в подклассе. Сегодня мы научимся делать именно это: создавать классы-шаблоны, нарушать которые просто невозможно.

⏱ ~45 минут 🎓 Для новичков 🐍 Python 3

😬 Проблема: забытый метод обнаружится слишком поздно

Представьте: вы разрабатываете систему для рисования фигур. Вы создаёте базовый класс Shape с методом area() — чтобы все фигуры умели считать свою площадь. Вы договорились с командой: «любая новая фигура обязана реализовать area()».

Через месяц новый разработчик пишет класс Triangle — и забывает про area(). Что произойдёт?

# Обычный базовый класс — просто «договорённость», которую можно нарушить
class Shape:
    def area(self):
        # Базовая реализация — заглушка.
        # Предполагается, что подкласс ДОЛЖЕН это переопределить.
        pass


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

    def area(self):
        return 3.14159 * self.radius ** 2


# Новый разработчик забыл реализовать area()!
class Triangle(Shape):
    def __init__(self, base, height):
        self.base = base
        self.height = height
    # Метод area() просто не написан. Python не возражает!


# Создаём объекты — всё выглядит нормально...
circle = Circle(5)
triangle = Triangle(4, 6)

print(circle.area())    # 78.53975  — правильный ответ ✅

# А вот здесь — неприятный сюрприз.
# Метод area() есть (унаследован от Shape), но возвращает None!
print(triangle.area())  # None  ❌ — тихая ошибка, очень сложно найти

🚨 В чём опасность?

Программа не падает с ошибкой — она молча возвращает None. Эту проблему можно не заметить неделями, особенно если метод вызывается редко или только в определённых условиях. Хотелось бы получить ошибку раньше — при создании объекта.

Именно для решения этой проблемы в Python существует модуль abc.

🧩 Абстрактный класс — шаблон, а не реальный объект

Абстрактный класс — это класс, который существует исключительно как шаблон, как контракт для своих подклассов. У него есть особое правило: создать его экземпляр нельзя.

Аналогия из жизни: представьте, что Shape — это понятие «фигура» в целом. Вы не можете взять в руки «просто фигуру» — нет такого предмета. Вы можете взять круг, квадрат, треугольник. «Фигура» — это только концепция, которая описывает общие свойства всех конкретных фигур.

🚫

Абстрактный класс

Это шаблон. Нельзя создать его экземпляр. Определяет, какие методы обязаны быть в подклассах.

Пример: Shape, Animal, Transport

✅

Конкретный класс

Это реализация. Наследует от абстрактного класса. Обязан реализовать все абстрактные методы.

Пример: Circle, Dog, Car

💡 Главная ценность абстрактного класса: если разработчик забудет реализовать обязательный метод, Python выдаст TypeError немедленно — в момент создания объекта, а не где-то позже в середине работы программы.

📦 Модуль abc: импортируем инструменты

В Python абстрактные классы реализуются с помощью встроенного модуля abc (Abstract Base Classes). Нам понадобится всего два инструмента из него:

🏛️

ABC

Базовый класс для абстрактных классов. Наш класс-шаблон должен наследовать от ABC, чтобы получить «абстрактную суперсилу».

🔖

@abstractmethod

Декоратор для методов. Помечает метод как «обязательный к переопределению». Тело метода в базовом классе обычно содержит только pass.

# Так выглядит импорт — всего одна строка
from abc import ABC, abstractmethod

📖 Что такое «декоратор»? Декоратор — это специальная пометка перед функцией (начинается с @), которая изменяет её поведение. @abstractmethod говорит Python: «этот метод — обязательный контракт, подкласс должен его реализовать». О декораторах подробнее — в отдельном уроке, пока просто запомните синтаксис.

✅ Решение: переписываем с ABC

Теперь перепишем наш пример с фигурами правильно. Добавим наследование от ABC и пометим area() как абстрактный метод:

from abc import ABC, abstractmethod


# Теперь Shape — это абстрактный класс.
# Шаг 1: наследуем от ABC
class Shape(ABC):

    # Шаг 2: помечаем метод как обязательный с помощью @abstractmethod
    @abstractmethod
    def area(self):
        """Возвращает площадь фигуры. Подкласс обязан реализовать этот метод."""
        pass  # Тело пустое — это просто объявление контракта


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

    # Выполняем контракт — реализуем area()
    def area(self):
        return 3.14159 * self.radius ** 2


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

    # Выполняем контракт — реализуем area()
    def area(self):
        return self.width * self.height


# Всё работает как нужно!
circle = Circle(5)
rectangle = Rectangle(4, 6)

print(circle.area())     # 78.53975  ✅
print(rectangle.area())  # 24  ✅

Отлично. А теперь — самое интересное. Посмотрим, что произойдёт, если нарушить контракт.

🚫 TypeError: нельзя создать экземпляр абстрактного класса

Первое правило: экземпляр абстрактного класса создать невозможно. Если попробовать — Python сразу выдаст TypeError:

from abc import ABC, abstractmethod


class Shape(ABC):
    @abstractmethod
    def area(self):
        pass


# Попытка создать экземпляр абстрактного класса
shape = Shape()
# TypeError: Can't instantiate abstract class Shape
# with abstract method area
#
# Перевод ошибки:
# «Невозможно создать экземпляр абстрактного класса Shape,
# у которого есть абстрактный метод area»

🎯 Почему это полезно? Это защищает вас от случайного использования «шаблона» напрямую. Shape — это не реальная фигура, это только схема. Python не позволит создать «просто Shape».

🔴 TypeError: не реализован @abstractmethod в подклассе

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

from abc import ABC, abstractmethod


class Shape(ABC):
    @abstractmethod
    def area(self):
        pass


# Разработчик забыл реализовать area() в Triangle
class Triangle(Shape):
    def __init__(self, base, height):
        self.base = base
        self.height = height
    # area() не написан!


# Теперь ошибка возникнет ЗДЕСЬ — при создании объекта
triangle = Triangle(4, 6)
# TypeError: Can't instantiate abstract class Triangle
# with abstract method area
#
# Мы получаем ошибку СРАЗУ, а не где-то в глубине программы!
# Это намного лучше, чем молчаливый None из первого примера.

💡 Сравните с первым примером (без ABC): без ABC метод area() «унаследовался» и вернул None — программа не упала, но дала неверный результат. С ABC Python поднимает TypeError ещё при создании объекта. Ошибка найдена сразу, до любого вызова метода.

Исправление простое — дописать метод:

from abc import ABC, abstractmethod


class Shape(ABC):
    @abstractmethod
    def area(self):
        pass


# Теперь исправляем Triangle — реализуем area()
class Triangle(Shape):
    def __init__(self, base, height):
        self.base = base
        self.height = height

    def area(self):
        return 0.5 * self.base * self.height  # Формула площади треугольника


# Теперь всё работает!
triangle = Triangle(4, 6)
print(triangle.area())  # 12.0  ✅

📋 Несколько абстрактных методов — полный контракт

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

Например, создадим абстрактный класс для животных, который требует реализации сразу трёх методов:

from abc import ABC, abstractmethod


class Animal(ABC):
    """Абстрактный класс-контракт для всех животных.
    Любой подкласс обязан реализовать ВСЕ три метода.
    """

    @abstractmethod
    def speak(self):
        """Как животное подаёт голос?"""
        pass

    @abstractmethod
    def move(self):
        """Как животное передвигается?"""
        pass

    @abstractmethod
    def eat(self):
        """Что животное ест?"""
        pass

    # Обычный метод — НЕ абстрактный.
    # Его переопределять НЕ обязательно.
    def describe(self):
        print(f"Я — {self.__class__.__name__}")


class Dog(Animal):
    # Реализуем все три обязательных метода
    def speak(self):
        print("Гав-гав! 🐶")

    def move(self):
        print("Бегу на четырёх лапах 🐾")

    def eat(self):
        print("Жую косточку 🦴")


class Bird(Animal):
    # Реализуем все три обязательных метода
    def speak(self):
        print("Чирик-чирик! 🐦")

    def move(self):
        print("Лечу, машу крыльями 🪶")

    def eat(self):
        print("Клюю зёрнышки 🌾")


dog = Dog()
bird = Bird()

dog.speak()     # Гав-гав! 🐶
dog.move()      # Бегу на четырёх лапах 🐾
dog.describe()  # Я — Dog  (унаследован от Animal)

bird.speak()    # Чирик-чирик! 🐦
bird.eat()      # Клюю зёрнышки 🌾

🧠 Важный момент: абстрактный класс может содержать не только абстрактные методы, но и обычные — с готовой реализацией. Такие методы можно использовать в подклассах напрямую (или переопределять). Абстрактными помечаются только те, что обязательно нужно реализовать.

🤝 Интерфейсы в Python — ABC как замена

Если вы слышали о языках Java или C#, то наверняка встречали слово интерфейс (interface). Давайте разберёмся, что это такое и почему в Python его нет — но ABC его успешно заменяет.

📄

Интерфейс (Java/C#)

Специальная конструкция языка. Содержит только объявления методов без реализации. Класс «реализует» интерфейс и обязан добавить все методы.

🏛️

ABC в Python

Абстрактный класс с методами, помеченными @abstractmethod. Достигает той же цели — обязательного контракта — и даже больше: может содержать готовые методы.

В Python нет отдельного ключевого слова interface. Вместо этого, если нужно создать чистый контракт (все методы абстрактные, никакой реализации) — создаётся ABC-класс, в котором все методы помечены @abstractmethod.

from abc import ABC, abstractmethod


# Это "интерфейс" в Python-стиле — все методы абстрактные
class Drawable(ABC):
    @abstractmethod
    def draw(self):
        pass

    @abstractmethod
    def resize(self, factor):
        pass


class Serializable(ABC):
    @abstractmethod
    def to_json(self):
        pass

    @abstractmethod
    def from_json(self, data):
        pass


# Python поддерживает множественное наследование!
# Класс может реализовывать несколько «интерфейсов» одновременно
class Button(Drawable, Serializable):
    def draw(self):
        print("Рисую кнопку 🔲")

    def resize(self, factor):
        print(f"Изменяю размер в {factor} раз")

    def to_json(self):
        return '{"type": "button"}'

    def from_json(self, data):
        print(f"Восстанавливаю из JSON: {data}")


btn = Button()
btn.draw()   # Рисую кнопку 🔲
btn.resize(2)
print(btn.to_json())

🎯 Ключевая мысль: В Python нет специального синтаксиса для интерфейсов. Абстрактный класс с набором @abstractmethod выполняет ту же роль. А поскольку в Python есть множественное наследование — один класс может «реализовывать» несколько таких «интерфейсов» одновременно.

🆕 typing.Protocol — современный подход (Python 3.8+)

В Python 3.8 появился ещё один способ описывать контракты — typing.Protocol. Он сочетает идеи ABC и утиной типизации и считается более современным подходом.

Главное отличие от ABC: с Protocol классу не нужно явно наследовать от протокола, чтобы соответствовать контракту. Достаточно просто иметь нужные методы — «утиная типизация» со структурной проверкой.

from typing import Protocol


# Определяем протокол — какие методы должен иметь объект
class Drawable(Protocol):
    def draw(self) -> None:
        ...  # Три точки — альтернатива pass в протоколах


# Обратите внимание: Circle НЕ наследует от Drawable!
# Он просто имеет нужный метод draw()
class Circle:
    def __init__(self, radius: float):
        self.radius = radius

    def draw(self) -> None:
        print(f"Рисую круг с радиусом {self.radius} 🔵")


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

    def draw(self) -> None:
        print(f"Рисую квадрат со стороной {self.side} 🟦")


# Функция принимает любой объект, соответствующий протоколу Drawable
def render(shape: Drawable) -> None:
    shape.draw()


render(Circle(5))    # Рисую круг с радиусом 5 🔵
render(Square(4))   # Рисую квадрат со стороной 4 🟦

Когда использовать ABC:

  • Нужно явное наследование и иерархия классов
  • Хотите, чтобы isinstance() работал
  • Нужны обычные методы с реализацией в базовом классе
  • Строгий контракт с принудительным наследованием

Когда использовать Protocol:

  • Нужна утиная типизация + аннотации типов
  • Не хотите заставлять классы явно наследовать
  • Проверка только статическим анализатором (mypy)
  • Более современные проекты, Python 3.8+

🧠 Для новичка: не нужно прямо сейчас глубоко погружаться в Protocol — просто знайте, что такой инструмент существует. Начните с ABC, это фундамент. Protocol — тема для более зрелого Python-кода и систем с аннотациями типов.

🧩 Полный пример: система оплаты

Давайте закрепим всё в одном реальном примере. Представьте систему оплаты, которая поддерживает несколько способов — банковскую карту, электронный кошелёк и криптовалюту. Каждый обязан реализовать методы pay() и refund().

from abc import ABC, abstractmethod


class PaymentMethod(ABC):
    """Базовый абстрактный класс для всех способов оплаты.
    Подкласс обязан реализовать pay() и refund().
    """

    @abstractmethod
    def pay(self, amount: float) -> bool:
        """Выполнить оплату. Возвращает True при успехе."""
        pass

    @abstractmethod
    def refund(self, amount: float) -> bool:
        """Выполнить возврат средств. Возвращает True при успехе."""
        pass

    # Обычный метод — общий для всех способов оплаты
    def get_name(self) -> str:
        return self.__class__.__name__


class CreditCard(PaymentMethod):
    def __init__(self, card_number: str):
        self.card_number = card_number

    def pay(self, amount: float) -> bool:
        print(f"💳 Оплата {amount}₽ с карты *{self.card_number[-4:]}")
        return True

    def refund(self, amount: float) -> bool:
        print(f"💳 Возврат {amount}₽ на карту *{self.card_number[-4:]}")
        return True


class EWallet(PaymentMethod):
    def __init__(self, email: str, balance: float):
        self.email = email
        self.balance = balance

    def pay(self, amount: float) -> bool:
        if self.balance >= amount:
            self.balance -= amount
            print(f"👛 Оплата {amount}₽ из кошелька {self.email}. Остаток: {self.balance}₽")
            return True
        print(f"👛 Недостаточно средств! Баланс: {self.balance}₽")
        return False

    def refund(self, amount: float) -> bool:
        self.balance += amount
        print(f"👛 Возврат {amount}₽ в кошелёк {self.email}. Новый баланс: {self.balance}₽")
        return True


# Функция оплаты работает с ЛЮБЫМ способом оплаты благодаря ABC
def process_order(payment: PaymentMethod, price: float):
    print(f"\n--- Обработка заказа через {payment.get_name()} ---")
    success = payment.pay(price)
    if success:
        print("✅ Заказ оплачен!")
    else:
        print("❌ Оплата не прошла")


card = CreditCard("4111111111111234")
wallet = EWallet("user@example.com", balance=500)

process_order(card, 299)
process_order(wallet, 199)
process_order(wallet, 400)  # Не хватит баланса!

🎯 Что здесь происходит: функция process_order принимает любой объект типа PaymentMethod. Она уверена: у этого объекта точно есть pay() и refund() — потому что ABC не позволит создать объект без них. Хотите добавить оплату криптовалютой? Напишите class Crypto(PaymentMethod), реализуйте два метода — и всё готово.

✅ Итоги урока

  • Абстрактный класс — это шаблон-контракт: нельзя создать его экземпляр напрямую, только через подкласс.
  • Без ABC: забытый метод наследуется и возвращает None — тихая ошибка, сложно найти.
  • С ABC: TypeError возникает сразу при создании объекта, если метод не реализован — ошибка найдена в начале.
  • Для использования ABC нужно импортировать: from abc import ABC, abstractmethod.
  • Наследование от ABC: class MyClass(ABC): — делает класс абстрактным.
  • @abstractmethod — декоратор, помечающий метод как обязательный к переопределению.
  • В Python нет ключевого слова interface — его роль играет ABC-класс, где все методы абстрактные.
  • typing.Protocol (Python 3.8+) — современная альтернатива: работает через утиную типизацию без явного наследования, подходит для кода с аннотациями типов.

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