Зачем использовать абстрактные базовые классы в 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

Он определит, поддерживает ли объект данный протокол, не проверяя наличие всех методов в протоколе или не вызывая чрезмерное исключение из глубины "вражеской" территории из-за отсутствия поддержки.