Абстрактные базовые классы Python (ABC)
Как Python-абстрактные базовые классы (ABC) обеспечивают единый интерфейс для подклассов с помощью @abstractmethod, абстрактных свойств и модуля abc.
Абстрактный класс — это класс, который нельзя создать напрямую. Он существует исключительно для определения общего интерфейса — набора методов, которые каждый конкретный подкласс обязан реализовать. Python предоставляет эту возможность через встроенный модуль abc (Abstract Base Classes).
В этой главе рассматриваются:
- Зачем нужны абстрактные классы и когда их использовать
- Модуль
abc—ABCи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.