Автор оригинала: 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, inprint('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
Смотрите также
- стандартная библиотечная документация для abc
- PEP 3119 – введение в абстрактные базовые классы
- коллекции – модуль коллекций включает абстрактные базовые классы для нескольких типов коллекций.
- PEP 3141 – иерархия типов для чисел
- Википедия: шаблон стратегии – описание и примеры шаблона стратегии, распространенного шаблона реализации подключаемого модуля.
- Динамические шаблоны кода: расширение ваших приложений с помощью подключаемых модулей – PyCon 2013 презентация Дуга Хеллмана
- Заметки о переносе Python 2 на 3 для abc