Абстрактные классы в Python

WebDev_Deep_6.5_site-5020-508885.png

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

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

Возвращаясь к проблеме со __slots__, решение через абстрактные классы выглядит примерно так:

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), наследуемся от абстрактных классов. Таким образом, мы, гарантируя совместимость интерфейсов, определяем независимые слоты для каждого из классов.

Автор
0 комментариев
Для комментирования необходимо авторизоваться