W3docs

Абстрактные базовые классы Python (ABC)

Как Python-абстрактные базовые классы (ABC) обеспечивают единый интерфейс для подклассов с помощью @abstractmethod, абстрактных свойств и модуля abc.

Абстрактный класс — это класс, который нельзя создать напрямую. Он существует исключительно для определения общего интерфейса — набора методов, которые каждый конкретный подкласс обязан реализовать. Python предоставляет эту возможность через встроенный модуль abc (Abstract Base Classes).

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

  • Зачем нужны абстрактные классы и когда их использовать
  • Модуль abcABC и abstractmethod
  • Принудительное соблюдение контрактов методов с помощью @abstractmethod
  • Абстрактные свойства
  • Конкретные методы внутри абстрактного класса
  • Абстрактные классы vs. вызов NotImplementedError
  • Пример из реальной практики

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

Зачем использовать абстрактные классы?

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

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

Абстрактные классы vs. вызов NotImplementedError

До появления модуля abc распространённым шаблоном было вызывать NotImplementedError в методе базового класса:

class Shape:
    def area(self):
        raise NotImplementedError("Subclasses must implement area()")

class Circle(Shape):
    pass  # forgot to implement area()

c = Circle()   # No error yet — Python lets this through
c.area()       # NotImplementedError only here, at call time

Недостаток: Python спокойно создаёт объект. Ошибка не видна до тех пор, пока метод не будет фактически вызван — что может произойти совершенно в другом месте программы.

С абстрактным классом ошибка возникает немедленно:

from abc import ABC, abstractmethod

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

class Circle(Shape):
    pass  # still missing area()

c = Circle()
# TypeError: Can't instantiate abstract class Circle
#            with abstract method area

Класс «сломан» в точке создания — именно там, где нарушен контракт.

Модуль abc

Модуль abc в Python предоставляет два инструмента, которые вы будете использовать постоянно:

ИмяЧто это
ABCВспомогательный базовый класс. Унаследуйтесь от него, чтобы сделать свой класс абстрактным.
abstractmethodДекоратор, который помечает метод как абстрактный.

Импортируйте их следующим образом:

from abc import ABC, abstractmethod

Создание абстрактного класса

Унаследуйтесь от ABC и украсьте хотя бы один метод декоратором @abstractmethod:

from abc import ABC, abstractmethod

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

    @abstractmethod
    def perimeter(self):
        pass

Теперь Shape объявляет, что всё, что называет себя фигурой, обязано предоставить area() и perimeter(). Создать Shape напрямую невозможно:

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

Реализация абстрактного класса

Конкретный класс — это класс, который наследуется от абстрактного и реализует все абстрактные методы. Только тогда можно создавать экземпляры:

Реализация абстрактного класса с Circle и Rectangle

from abc import ABC, abstractmethod
import math

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

    @abstractmethod
    def perimeter(self):
        pass

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

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

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

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

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

    def perimeter(self):
        return 2 * (self.width + self.height)

c = Circle(5)
r = Rectangle(4, 6)

print(round(c.area(), 4))       # 78.5398
print(round(c.perimeter(), 4))  # 31.4159
print(r.area())                 # 24
print(r.perimeter())            # 20

И Circle, и Rectangle реализуют все абстрактные методы, поэтому Python разрешает создавать их экземпляры.

Что происходит, если пропустить метод?

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

from abc import ABC, abstractmethod

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

    @abstractmethod
    def perimeter(self):
        pass

class IncompleteShape(Shape):
    def area(self):
        return 0
    # perimeter() not implemented

s = IncompleteShape()
# TypeError: Can't instantiate abstract class IncompleteShape
#            with abstract method perimeter

Сообщение об ошибке точно указывает, какой метод забыт — это гораздо полезнее, чем обобщённый сбой во время выполнения.

Абстрактные свойства

Вы также можете сделать свойство абстрактным. Это вынуждает подклассы предоставить свойство (или атрибут, вычисляемый через свойство), а не просто обычный метод. Используйте @property и @abstractmethod вместе, причём @property должен быть сверху:

Использование абстрактных свойств

from abc import ABC, abstractmethod

class Animal(ABC):
    @property
    @abstractmethod
    def sound(self):
        pass

    def describe(self):
        return f"I make the sound: {self.sound}"

class Dog(Animal):
    @property
    def sound(self):
        return "Woof"

class Cat(Animal):
    @property
    def sound(self):
        return "Meow"

dog = Dog()
cat = Cat()

print(dog.describe())   # I make the sound: Woof
print(cat.describe())   # I make the sound: Meow

Свойство sound в каждом подклассе с точки зрения вызывающей стороны ведёт себя как атрибут только для чтения, тогда как абстрактный класс гарантирует, что каждый конкретный подкласс его предоставит.

Конкретные методы в абстрактном классе

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

Совместное использование общей логики через конкретный метод

from abc import ABC, abstractmethod

class Logger(ABC):
    def log(self, message):
        """Concrete method — shared by all subclasses."""
        formatted = self.format_message(message)
        self.write(formatted)

    def format_message(self, message):
        return f"[LOG] {message}"

    @abstractmethod
    def write(self, message):
        """Abstract — each subclass decides how to output."""
        pass

class ConsoleLogger(Logger):
    def write(self, message):
        print(message)

class FileLogger(Logger):
    def __init__(self):
        self.entries = []

    def write(self, message):
        self.entries.append(message)

console = ConsoleLogger()
console.log("Server started")      # prints: [LOG] Server started

file_log = FileLogger()
file_log.log("Database connected")
print(file_log.entries)            # ['[LOG] Database connected']

Здесь log() и format_message() — конкретные общие методы. Только write() — та часть, которая различается у разных реализаций — является абстрактной. Подклассам никогда не нужно заново реализовывать логику форматирования.

isinstance() и абстрактные классы

Объект, являющийся экземпляром конкретного подкласса, также считается экземпляром абстрактного базового класса. Это позволяет писать функции, принимающие любой объект, соответствующий интерфейсу, без привязки к конкретному типу:

from abc import ABC, abstractmethod
import math

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

    @abstractmethod
    def perimeter(self):
        pass

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

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

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

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

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

    def perimeter(self):
        return 2 * (self.width + self.height)

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

for shape in shapes:
    print(isinstance(shape, Shape))   # True for both

def total_area(shapes):
    return sum(s.area() for s in shapes)

print(round(total_area(shapes), 4))  # 48.2743

В этом и состоит основное преимущество: total_area() работает с любой Shape — прошлой, настоящей или будущей — при условии, что объект реализует интерфейс.

Пример из реальной практики: платёжные процессоры

Абстрактные классы особенно полезны, когда нужно несколько реализаций одной концепции. Рассмотрим платёжную систему, которая должна поддерживать несколько провайдеров:

Абстрактный класс для обработки платежей

from abc import ABC, abstractmethod

class PaymentProcessor(ABC):
    @abstractmethod
    def charge(self, amount):
        pass

    @abstractmethod
    def refund(self, amount):
        pass

    def process(self, amount):
        """Concrete method — shared processing logic."""
        print(f"Processing payment of ${amount}")
        self.charge(amount)

class StripeProcessor(PaymentProcessor):
    def charge(self, amount):
        print(f"Stripe: charged ${amount}")

    def refund(self, amount):
        print(f"Stripe: refunded ${amount}")

class PayPalProcessor(PaymentProcessor):
    def charge(self, amount):
        print(f"PayPal: charged ${amount}")

    def refund(self, amount):
        print(f"PayPal: refunded ${amount}")

processors = [StripeProcessor(), PayPalProcessor()]

for p in processors:
    p.process(100)
# Processing payment of $100
# Stripe: charged $100
# Processing payment of $100
# PayPal: charged $100

Добавление третьего платёжного провайдера — например, BraintreeProcessor — требует лишь реализации charge() и refund(). Логика process() и любые проверки isinstance продолжат работать без изменений.

Когда использовать абстрактные классы

Используйте абстрактный класс, когда:

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

Абстрактный класс, вероятно, не нужен, когда:

  • У вас лишь один-два подкласса и иерархия вряд ли будет расти.
  • Общий интерфейс уже обеспечивается другими средствами (например, датаклассами Python или утиной типизацией).
  • Вы просто хотите запретить прямое создание экземпляров — более простым решением является хук __init_subclass__ или обычный комментарий.

По близким темам смотрите наследование в Python и полиморфизм в Python.

Практика

Практика
What happens when you try to instantiate a class that inherits from ABC but does not implement all abstract methods?
What happens when you try to instantiate a class that inherits from ABC but does not implement all abstract methods?
Was this page helpful?