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

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

Не пропустите новые полезные статьи!

Спасибо за подписку!

Мы отправили вам письмо для подтверждения вашего email.
С уважением, OTUS!

Автор
1 комментарий
0

Здравствуйте! Очень интересная статья. После прочтения решил померить размер экземпляров классов при помощи pympler.asizeof: - наследованный от одного класса на slots 16b - наследованный от двух классов на dict 168b - наследованный от двух абстрактных классов на slots - 440b !!! Соответственно вопрос: Какой смысл в таком наследовании?

Для комментирования необходимо авторизоваться
Популярное
Сегодня тут пусто
Новое направление: OTUS Kids 👧👦
Курсы для подростков от 10 до 18 лет →