Рубрики
Без рубрики

abc – абстрактные базовые классы

Автор оригинала: Doug Hellmann.

Цель:

Определите и используйте абстрактные базовые классы для проверки интерфейса.

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

Абстрактные базовые классы – это форма проверки интерфейса, более строгая, чем отдельные проверки hasattr () для определенных методов. Определив абстрактный базовый класс, можно установить общий API для набора подклассов. Эта возможность особенно полезна в ситуациях, когда кто-то, менее знакомый с исходным кодом приложения, собирается предоставить расширения подключаемых модулей, но также может помочь при работе в большой команде или с большой базой кода, где отслеживаются все занятия одновременно сложно или невозможно.

Как работают азбуки

abc работает, отмечая методы базового класса как абстрактные, а затем регистрируя конкретные классы как реализации абстрактной базы. Если приложению или библиотеке требуется конкретный API, можно использовать issubclass () или isinstance () для проверки объекта на соответствие абстрактному классу.

Для начала определите абстрактный базовый класс для представления API набора подключаемых модулей для сохранения и загрузки данных. Установите для метакласса нового базового класса значение ABCMeta и используйте декораторы, чтобы установить общедоступный API для класса. В следующих примерах используется abc_base.py .

abc_base.py

import abc


class PluginBase(metaclassabc.ABCMeta):

    @abc.abstractmethod
    def load(self, input):
        """Retrieve data from the input source
        and return an object.
        """

    @abc.abstractmethod
    def save(self, output, data):
        """Save the data object to the output."""

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

Есть два способа указать, что конкретный класс реализует абстрактный API: либо явно зарегистрировать класс, либо создать новый подкласс непосредственно из абстрактной базы. Используйте метод класса register () в качестве декоратора для конкретного класса, чтобы добавить его явно, когда класс предоставляет требуемый API, но не является частью дерева наследования абстрактного базового класса.

abc_register.py

import abc
from abc_base import PluginBase


class LocalBaseClass:
    pass


@PluginBase.register
class RegisteredImplementation(LocalBaseClass):

    def load(self, input):
        return input.read()

    def save(self, output, data):
        return output.write(data)


if __name__  '__main__':
    print('Subclass:', issubclass(RegisteredImplementation,
                                  PluginBase))
    print('Instance:', isinstance(RegisteredImplementation(),
                                  PluginBase))

В этом примере RegisteredImplementation является производным от LocalBaseClass , но зарегистрирован как реализующий API PluginBase , поэтому issubclass () а isinstance () обрабатывает его как производный от PluginBase .

$ python3 abc_register.py

Subclass: True
Instance: True

Реализация через подклассы

Создание подклассов непосредственно из базы позволяет избежать явной регистрации класса.

abc_subclass.py

import abc
from abc_base import PluginBase


class SubclassImplementation(PluginBase):

    def load(self, input):
        return input.read()

    def save(self, output, data):
        return output.write(data)


if __name__  '__main__':
    print('Subclass:', issubclass(SubclassImplementation,
                                  PluginBase))
    print('Instance:', isinstance(SubclassImplementation(),
                                  PluginBase))

В этом случае обычные функции управления классами Python используются для распознавания SubclassImplementation как реализации абстрактной PluginBase .

$ python3 abc_subclass.py

Subclass: True
Instance: True

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

abc_find_subclasses.py

import abc
from abc_base import PluginBase
import abc_subclass
import abc_register

for sc in PluginBase.__subclasses__():
    print(sc.__name__)

Несмотря на то, что abc_register () импортирован, RegisteredImplementation не входит в список подклассов, поскольку фактически не является производным от базового.

$ python3 abc_find_subclasses.py

SubclassImplementation

Базовый класс помощника

Если вы забыли правильно установить метакласс, это означает, что конкретные реализации не имеют принудительных API. Чтобы упростить правильную настройку абстрактного класса, предоставляется базовый класс, который автоматически устанавливает метакласс.

abc_abc_base.py

import abc


class PluginBase(abc.ABC):

    @abc.abstractmethod
    def load(self, input):
        """Retrieve data from the input source
        and return an object.
        """

    @abc.abstractmethod
    def save(self, output, data):
        """Save the data object to the output."""


class SubclassImplementation(PluginBase):

    def load(self, input):
        return input.read()

    def save(self, output, data):
        return output.write(data)


if __name__  '__main__':
    print('Subclass:', issubclass(SubclassImplementation,
                                  PluginBase))
    print('Instance:', isinstance(SubclassImplementation(),
                                  PluginBase))

Чтобы создать новый абстрактный класс, просто наследуйте от ABC .

$ python3 abc_abc_base.py

Subclass: True
Instance: True

Неполные реализации

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

abc_incomplete.py

import abc
from abc_base import PluginBase


@PluginBase.register
class IncompleteImplementation(PluginBase):

    def save(self, output, data):
        return output.write(data)


if __name__  '__main__':
    print('Subclass:', issubclass(IncompleteImplementation,
                                  PluginBase))
    print('Instance:', isinstance(IncompleteImplementation(),
                                  PluginBase))

Это предотвращает запуск неполных реализаций непредвиденных ошибок во время выполнения.

$ python3 abc_incomplete.py

Subclass: True
Traceback (most recent call last):
  File "abc_incomplete.py", line 24, in 
    print('Instance:', isinstance(IncompleteImplementation(),
TypeError: Can't instantiate abstract class
IncompleteImplementation with abstract methods load

Конкретные методы в азбуке

Хотя конкретный класс должен предоставлять реализации всех абстрактных методов, абстрактный базовый класс также может предоставлять реализации, которые можно вызывать с помощью super () . Это позволяет повторно использовать общую логику, помещая ее в базовый класс, но вынуждает подклассы предоставлять замещающий метод с (потенциально) настраиваемой логикой.

abc_concrete_method.py

import abc
import io


class ABCWithConcreteImplementation(abc.ABC):

    @abc.abstractmethod
    def retrieve_values(self, input):
        print('base class reading data')
        return input.read()


class ConcreteOverride(ABCWithConcreteImplementation):

    def retrieve_values(self, input):
        base_data  super(ConcreteOverride,
                          self).retrieve_values(input)
        print('subclass sorting data')
        response  sorted(base_data.splitlines())
        return response


input  io.StringIO("""line one
line two
line three
""")

reader  ConcreteOverride()
print(reader.retrieve_values(input))
print()

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

$ python3 abc_concrete_method.py

base class reading data
subclass sorting data
['line one', 'line three', 'line two']

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

Если спецификация API включает атрибуты в дополнение к методам, она может потребовать атрибуты в конкретных классах, объединив abstractmethod () с property () .

abc_abstractproperty.py

import abc


class Base(abc.ABC):

    @property
    @abc.abstractmethod
    def value(self):
        return 'Should never reach here'

    @property
    @abc.abstractmethod
    def constant(self):
        return 'Should never reach here'


class Implementation(Base):

    @property
    def value(self):
        return 'concrete property'

    constant  'set by a class attribute'


try:
    b  Base()
    print('Base.value:', b.value)
except Exception as err:
    print('ERROR:', str(err))

i  Implementation()
print('Implementation.value   :', i.value)
print('Implementation.constant:', i.constant)

Класс Base в этом примере не может быть создан, поскольку он имеет только абстрактную версию методов получения свойств для value и constant . Свойству value дается конкретный получатель в Implementation , а constant определяется с помощью атрибута класса.

$ python3 abc_abstractproperty.py

ERROR: Can't instantiate abstract class Base with abstract
methods constant, value
Implementation.value   : concrete property
Implementation.constant: set by a class attribute

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

abc_abstractproperty_rw.py

import abc


class Base(abc.ABC):

    @property
    @abc.abstractmethod
    def value(self):
        return 'Should never reach here'

    @value.setter
    @abc.abstractmethod
    def value(self, new_value):
        return


class PartialImplementation(Base):

    @property
    def value(self):
        return 'Read-only'


class Implementation(Base):

    _value  'Default value'

    @property
    def value(self):
        return self._value

    @value.setter
    def value(self, new_value):
        self._value  new_value


try:
    b  Base()
    print('Base.value:', b.value)
except Exception as err:
    print('ERROR:', str(err))

p  PartialImplementation()
print('PartialImplementation.value:', p.value)

try:
    p.value  'Alteration'
    print('PartialImplementation.value:', p.value)
except Exception as err:
    print('ERROR:', str(err))

i  Implementation()
print('Implementation.value:', i.value)

i.value  'New value'
print('Changed value:', i.value)

Конкретное свойство должно быть определено так же, как абстрактное свойство, как для чтения-записи, так и только для чтения. Замена свойства чтения-записи в PartialImplementation на свойство, доступное только для чтения, оставляет свойство доступным только для чтения – метод установки свойства из базового класса не используется повторно.

$ python3 abc_abstractproperty_rw.py

ERROR: Can't instantiate abstract class Base with abstract
methods value
PartialImplementation.value: Read-only
ERROR: can't set attribute
Implementation.value: Default value
Changed value: New value

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

Абстрактный класс и статические методы

Классовые и статические методы также можно пометить как абстрактные.

abc_class_static.py

import abc


class Base(abc.ABC):

    @classmethod
    @abc.abstractmethod
    def factory(cls, *args):
        return cls()

    @staticmethod
    @abc.abstractmethod
    def const_behavior():
        return 'Should never reach here'


class Implementation(Base):

    def do_something(self):
        pass

    @classmethod
    def factory(cls, *args):
        obj  cls(*args)
        obj.do_something()
        return obj

    @staticmethod
    def const_behavior():
        return 'Static behavior differs'


try:
    o  Base.factory()
    print('Base.value:', o.const_behavior())
except Exception as err:
    print('ERROR:', str(err))

i  Implementation.factory()
print('Implementation.const_behavior :', i.const_behavior())

Хотя метод класса вызывается для класса, а не для экземпляра, он по-прежнему предотвращает создание экземпляра класса, если он не определен.

$ python3 abc_class_static.py

ERROR: Can't instantiate abstract class Base with abstract
methods const_behavior, factory
Implementation.const_behavior : Static behavior differs

Смотрите также