Абстрактные классы в Python
В прошлый раз, рассматривая принципы работы со слотами в классах, мы столкнулись с проблемой множественного наследования.
Суть проблемы заключалась в том, что если у двух классов определён атрибут
class BaseA: __slots__ = ('a',) class BaseB: __slots__ = ('b',) >>> class Child(BaseA, BaseB): __slots__ = () Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: multiple bases have instance lay-out conflict
Можно, конечно, не указывать слоты в родительских классах и заполнить их только в дочернем, но это частный случай. Что же делать, если слоты нужны во всех трёх классах?
Как раз для таких (хотя и не только) случаев в ООП есть принцип абстрагирования. Правда, в Python на уровне абстракции не реализованы, но в стандартную библиотеку входит модуль abc — Abstract Base Classes.
Абстрактный класс сам по себе нельзя инстанцировать — в нём определяется, какие методы и свойства нужно будет переопределить в дочерних классах.
from abc import ABC, abstractmethod, abstractproperty class AbstractBase(ABC): @abstractmethod def foo(self): pass @abstractproperty def baz(self): pass >>> AbstractBase() Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: Can't instantiate abstract class AbstractBase with abstract methods baz, foo class Base(AbstractBase): def foo(self): print('foo') @property def baz(self): return 'baz' >>> base = Base() >>> base.foo() foo >>> base.baz 'baz'
Благодаря использованию абстрактных классов мы можем проконтролировать, что все дочерние классы имеют одинаковый интерфейс. Проще всего это понять на примере.
class SerialPort(ABC): @abstractmethod def read(self): pass @abstractmethod def write(self): pass
Мы создали абстрактный класс SerialPort. Теперь, наследуя от него разные реализации (COM, USB, USB-C), мы не будем обязаны реализовать 2 базовых метода. Это будет гарантировать, что реализации всех конкретных классов можно использовать одинаково, не заботясь о том, какой именно последовательный порт используется.
class COM(SerialPort): pass >>> com = COM() Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: Can't instantiate abstract class COM with abstract methods read, write class COM(SerialPort): def read(self): return "" def write(self): pass >>> com = COM() >>> com.read() ''
Рассмотрим множественное наследование от абстрактных классов:
class Charger(ABC): @abstractmethod def charge(self): pass class USB(SerialPort, Charger): def read(self): return "" def write(self): pass >>> usb = USB() Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: Can't instantiate abstract class USB with abstract methods charge
Ага, класс USB должен обязательно иметь имплементацию метода charge, поскольку это задекларировано в родительском классе Charger:
class USB(SerialPort, Charger): def read(self): return "" def write(self): pass def charge(self): print('Charging') >>> usb = USB() >>> usb.charge() Charging
Возвращаясь к проблеме со
class AbstractA(ABC): __slots__ = () class AbstractB(ABC): __slots__ = () class BaseA(AbstractA): __slots__ = ('a',) class BaseB(AbstractB): __slots__ = ('b',) class Child(AbstractA, AbstractB): __slots__ = ('a', 'b') c = Child()
То есть мы вместо того, чтобы наследоваться от классов с конкретной реализацией (BaseA, BaseB), наследуемся от абстрактных классов. Таким образом, мы, гарантируя совместимость интерфейсов, определяем независимые слоты для каждого из классов.