Зачем использовать абстрактные базовые классы в Python?
Будучи привыкшим к старым способам утиной печати на Python, я не понял необходимости использования ABC (абстрактные базовые классы). help хороша в том, как их использовать.
Я попытался прочитать обоснование в PEP, но это пошло мне на голову. Если бы я искал контейнер с изменяемой последовательностью, я бы посмотрел на __setitem__
или, скорее всего, попытался его использовать (EAFP). Я не нашел реального использования для модуля numbers, который использует ABC, но это самое близкое мне понимание.
Может ли кто-нибудь объяснить мне обоснование, пожалуйста?
Ответы
Ответ 1
Краткая версия
ABC предлагают более высокий уровень семантического контракта между клиентами и реализованными классами.
Длинная версия
Существует договор между классом и его абонентами. Класс promises выполняет определенные действия и обладает определенными свойствами.
В контракте есть разные уровни.
На очень низком уровне контракт может включать имя метода или его количество параметров.
В статично типизированном языке этот контракт фактически будет реализован компилятором. В Python вы можете использовать EAFP или интроспекцию, чтобы подтвердить, что неизвестный объект соответствует этому ожидаемому контракту.
Но в контракте есть также более высокий уровень, семантический promises.
Например, если существует метод __str__()
, ожидается, что он вернет строковое представление объекта. Он может удалить все содержимое объекта, совершить транзакцию и вырвать пустую страницу из принтера... но есть общее понимание того, что она должна делать, описанная в руководстве Python.
Это особый случай, когда семантический договор описан в руководстве. Что должен сделать метод print()
? Должен ли он записывать объект на принтер или строку на экран или что-то еще? Это зависит - вы должны прочитать комментарии, чтобы понять полный контракт здесь. Кусок клиентского кода, который просто проверяет существование метода print()
, подтвердил часть контракта - что вызов метода может быть выполнен, но не означает, что существует соглашение о семантике более высокого уровня вызова.
Определение абстрактного базового класса (ABC) - это способ создания контракта между разработчиками классов и вызывающими. Это не просто список имен методов, но и общее понимание того, что должны делать эти методы. Если вы наследуете эту ABC, вы обещаете следовать всем правилам, описанным в комментариях, включая семантику метода print()
.
Утилизация утинов Python имеет много преимуществ в гибкости по сравнению с статическим типом, но она не решает всех проблем. ABC предлагают промежуточное решение между свободной формой Python и связью и дисциплиной статически типизированного языка.
Ответ 2
@Неверный ответ, но я думаю, что он пропустил реальную практическую причину. У Python есть ABC в мире утиного ввода.
Абстрактные методы являются аккуратными, но, на мой взгляд, они действительно не заполняют какие-либо прецеденты, которые уже не покрыты печатанием утки. Реальная базовая категория абстрактных классов находится в способом, позволяющим вам настроить поведение isinstance
и issubclass
. (__subclasshook__
- это, в основном, более удобный API поверх Python __instancecheck__
и __subclasscheck__
перехватывает.) Адаптация встроенных конструкций для работы пользовательские типы - это очень большая часть философии Python.
Исходный код Python является образцовым. Здесь описано, как collections.Container
определяется в стандартной библиотеке (на момент написания):
class Container(metaclass=ABCMeta):
__slots__ = ()
@abstractmethod
def __contains__(self, x):
return False
@classmethod
def __subclasshook__(cls, C):
if cls is Container:
if any("__contains__" in B.__dict__ for B in C.__mro__):
return True
return NotImplemented
В этом определении __subclasshook__
говорится, что любой класс с атрибутом __contains__
считается подклассом Container, даже если он не подклассифицирует его напрямую. Поэтому я могу написать это:
class ContainAllTheThings(object):
def __contains__(self, item):
return True
>>> issubclass(ContainAllTheThings, collections.Container)
True
>>> isinstance(ContainAllTheThings(), collections.Container)
True
Другими словами, если вы реализуете правильный интерфейс, вы являетесь подклассом! ABC предоставляют формальный способ определения интерфейсов в Python, сохраняя при этом верность духу набора текста. Кроме того, это работает таким образом, который отличает принцип Open-Closed.
Объектная модель Python внешне внешне похожа на модель более "традиционной" OO (под которой я подразумеваю Java *) - мы получили ваши классы, ваши объекты, ваши методы, но когда вы поцарапаете поверхность, вы найдете что-то гораздо богаче и гибче. Точно так же понятие Python абстрактных базовых классов может быть узнано для Java-разработчика, но на практике они предназначены для совершенно другой цели.
Иногда я нахожу себя в написании полиморфных функций, которые могут действовать на одном элементе или наборе элементов, и я нахожу isinstance(x, collections.Iterable)
более читаемым, чем hasattr(x, '__iter__')
или эквивалентный блок try...except
. (Если вы не знали Python, кто из этих трех сделал бы код чистым?)
Я нахожу, что мне редко приходится писать свою собственную ABC - я предпочитаю полагаться на утиную печать - и я обычно обнаруживаю необходимость в одном, используя рефакторинг. Если я вижу полиморфную функцию, выполняющую множество проверок атрибутов или множество функций, выполняющих одни и те же проверки атрибутов, этот запах указывает на существование ожидающей выделения ABC.
*, не вдаваясь в дискуссию о том, является ли Java "традиционной" системой OO...
Добавление. Хотя абстрактный базовый класс может переопределить поведение isinstance
и issubclass
, он все равно не вводит MRO виртуального подкласса. Это потенциальная ошибка для клиентов: не каждый объект, для которого isinstance(x, MyABC) == True
имеет методы, определенные на MyABC
.
class MyABC(metaclass=abc.ABCMeta):
def abc_method(self):
pass
@classmethod
def __subclasshook__(cls, C):
return True
class C(object):
pass
# typical client code
c = C()
if isinstance(c, MyABC): # will be true
c.abc_method() # raises AttributeError
К сожалению, одна из этих "просто не делает" ловушек (из которых у Python относительно мало!): избегайте определения ABC с помощью __subclasshook__
и не абстрактных методов. Более того, вы должны сделать свое определение __subclasshook__
совместимым с набором абстрактных методов, которые определяет ваш ABC.
Ответ 3
Удобная функция ABC заключается в том, что если вы не реализуете все необходимые методы (и свойства), вы получаете ошибку при создании экземпляра, а не AttributeError
, возможно, намного позже, когда вы на самом деле пытаетесь использовать отсутствующий метод.
from abc import ABCMeta, abstractmethod
class Base(object):
__metaclass__ = ABCMeta
@abstractmethod
def foo(self):
pass
@abstractmethod
def bar(self):
pass
class Concrete(Base):
def foo(self):
pass
# We forget to declare `bar`
c = Concrete()
# TypeError: "Can't instantiate abstract class Concrete with abstract methods bar"
Пример из https://dbader.org/blog/abstract-base-classes-in-python
Ответ 4
Он определит, поддерживает ли объект данный протокол, не проверяя наличие всех методов в протоколе или не вызывая чрезмерное исключение из глубины "вражеской" территории из-за отсутствия поддержки.