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

213

Поскольку я привык к старым способам типизации уток в Python, я не понимаю необходимости ABC (абстрактные базовые классы). Помощь хорошо о том , как использовать их.

Я попытался прочитать обоснование в ПКП , но это пошло мне на ум. Если бы я искал изменяемый контейнер последовательности, я бы проверил его __setitem__или, скорее, попытался бы его использовать ( EAFP ). Я не сталкивался с реальным использованием модуля чисел , который использует азбуку, но это самое близкое к пониманию.

Может кто-нибудь объяснить мне обоснование, пожалуйста?

Мухаммед Алкарури
источник

Ответы:

162

Укороченная версия

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

Длинная версия

Существует договор между классом и его посетителями. Класс обещает делать определенные вещи и обладать определенными свойствами.

Существуют разные уровни контракта.

На очень низком уровне контракт может содержать название метода или его количество параметров.

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

Но в контракте также есть семантические обещания более высокого уровня.

Например, если есть __str__()метод, ожидается, что он вернет строковое представление объекта. Он может удалить все содержимое объекта, зафиксировать транзакцию и выплюнуть пустую страницу из принтера ... но существует общее понимание того, что он должен делать, описанное в руководстве по Python.

Это особый случай, когда семантический контракт описан в руководстве. Что должен print()делать метод? Должен ли он записать объект на принтер или строку на экран, или что-то еще? Это зависит - вам нужно прочитать комментарии, чтобы понять полный контракт здесь. Часть клиентского кода, которая просто проверяет, print()существует ли метод, подтвердила часть контракта - что вызов метода может быть выполнен, но не то, что существует соглашение о семантике вызова более высокого уровня.

Определение абстрактного базового класса (ABC) - это способ создания контракта между реализациями класса и вызывающими. Это не просто список имен методов, а общее понимание того, что эти методы должны делать. Если вы наследуете от этого ABC, вы обещаете следовать всем правилам, описанным в комментариях, включая семантику print()метода.

Утиная печать Python имеет много преимуществ в гибкости по сравнению со статической, но она не решает всех проблем. Азбука предлагает промежуточное решение между свободной формой Python и связыванием и дисциплиной статически типизированного языка.

Oddthinking
источник
12
Я думаю, что у вас есть смысл, но я не могу следовать за вами. Так в чем же разница, с точки зрения контракта, между классом, который реализует, __contains__и классом, который наследуется collections.Container? В вашем примере в Python всегда было общее понимание __str__. Реализация __str__дает те же обещания, что и наследование от некоторого ABC, а затем реализация __str__. В обоих случаях вы можете нарушить договор; нет доказуемой семантики, такой как у нас в статической типизации.
Мухаммед Алкарури
15
collections.Containerявляется вырожденным случаем, который включает в себя \_\_contains\_\_и только означает предопределенное соглашение. Я согласен, что использование ABC само по себе не приносит особой пользы. Я подозреваю, что это было добавлено, чтобы позволить (например) Setнаследовать от него. К тому времени, когда вы добираетесь до места Set, внезапно принадлежность к ABC имеет значительную семантику. Предмет не может принадлежать коллекции дважды. Это НЕ обнаруживается существованием методов.
Нечетное
3
Да, я думаю Set, это лучший пример, чем print(). Я пытался найти имя метода, значение которого было неоднозначным, и его нельзя было обмануть одним только именем, поэтому вы не могли быть уверены, что он будет делать правильные вещи только по его имени и руководству по Python.
Нечетное
3
Есть ли шанс переписать ответ Setв качестве примера вместо print? Setимеет много смысла, @Oddthinking.
Эхтеш Чоудхури
1
Я думаю, что эта статья объясняет это очень хорошо: dbader.org/blog/abstract-base-classes-in-python
szabgab
228

@ Ответ Oddthinking не является неправильным, но я думаю , что не попадает в реальный , практический разум Python имеет азбуку в мире утка-типирования.

Абстрактные методы аккуратны, но, на мой взгляд, они на самом деле не заполняют ни одного варианта использования, еще не охваченного типизацией утки. Реальная сила абстрактных базовых классов заключается в том, как они позволяют настраивать поведение 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__атрибутом считается подклассом контейнера, даже если он не подкласс его напрямую. Так что я могу написать это:

class ContainAllTheThings(object):
    def __contains__(self, item):
        return True

>>> issubclass(ContainAllTheThings, collections.Container)
True
>>> isinstance(ContainAllTheThings(), collections.Container)
True

Другими словами, если вы реализуете правильный интерфейс, вы подкласс! ABC предоставляют формальный способ определения интерфейсов в Python, оставаясь верным духу утиной типизации. Кроме того, это работает таким образом, чтобы соблюдать принцип Open-Closed .

Объектная модель Python внешне похожа на модель более «традиционной» ОО-системы (под которой я имею в виду Java *) - у нас есть ваши классы, ваши объекты, ваши методы - но когда вы поцарапаете поверхность, вы обнаружите что-то гораздо более богатое и более гибкий. Аналогично, понятие Python об абстрактных базовых классах может быть узнаваемым для разработчика Java, но на практике они предназначены для совсем другой цели.

Я иногда нахожу себя пишущим полиморфные функции, которые могут воздействовать на отдельный элемент или набор элементов, и я нахожу, isinstance(x, collections.Iterable)что гораздо более читабельным, чем hasattr(x, '__iter__')или эквивалентный try...exceptблок. (Если бы вы не знали Python, какой из этих трех определил бы смысл кода?)

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

* не вдаваясь в споры о том, является ли Java «традиционной» ОО-системой ...


Приложение : Хотя абстрактный базовый класс может переопределять поведение 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 как a, так __subclasshook__и неабстрактными методами. Более того, вы должны привести свое определение в __subclasshook__соответствие с набором абстрактных методов, которые определяет ваша ABC.

Бенджамин Ходжсон
источник
21
«Если вы реализуете правильный интерфейс, вы подкласс» Большое спасибо за это. Я не знаю, пропустил ли это странное мышление, но я точно знал. FWIW, isinstance(x, collections.Iterable)мне понятнее, и я знаю Python.
Мухаммед Алкарури
Отличный пост. Спасибо. Я думаю, что Дополнение, «просто не делайте этого», немного похоже на обычное наследование подкласса, но затем Cудаление подкласса (или повреждение без возможности восстановления) abc_method()унаследованного от MyABC. Принципиальное отличие заключается в том, что испортить договор наследования является суперкласс, а не подкласс.
Майкл Скотт Катберт
Разве вам не нужно делать, Container.register(ContainAllTheThings)чтобы данный пример работал?
BoZenKhaa
1
@BoZenKhaa Код в ответе работает! Попытайся! Значение __subclasshook__«любой класс, который удовлетворяет этому предикату, считается подклассом для целей isinstanceи issubclassпроверок, независимо от того, был ли он зарегистрирован в ABC и независимо от того, является ли он прямым подклассом ». Как я сказал в ответе, если вы реализуете правильный интерфейс, вы подкласс!
Бенджамин Ходжсон
1
Вы должны изменить форматирование с курсива на жирный. «Если вы реализуете правильный интерфейс, вы подкласс!» Очень краткое объяснение. Спасибо!
Марти
108

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

from abc import ABCMeta, abstractmethod

# python2
class Base(object):
    __metaclass__ = ABCMeta

    @abstractmethod
    def foo(self):
        pass

    @abstractmethod
    def bar(self):
        pass

# python3
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

Изменить: включить синтаксис Python3, спасибо @PandasRocks

cerberos
источник
5
Пример и ссылка были очень полезны. Спасибо!
nalyd88
если Base определен в отдельном файле, вы должны наследовать его как Base.Base или изменить строку импорта на «из Base import Base»
Райан Теннилл,
Следует отметить, что в Python3 синтаксис немного отличается. Смотрите этот ответ: stackoverflow.com/questions/28688784/…
PandasRocks
7
Исходя из C # фона, это причина использовать абстрактные классы. Вы предоставляете функциональность, но заявляете, что эта функциональность требует дальнейшей реализации. Другие ответы, кажется, упускают этот момент.
Джош Ноу
18

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

Игнасио Васкес-Абрамс
источник
6

Абстрактный метод должен убедиться, что любой метод, который вы вызываете в родительском классе, должен присутствовать в дочернем классе. Ниже приведены способы вызова noraml и использования тезисов. Программа написана на python3

Нормальный способ звонка

class Parent:
def methodone(self):
    raise NotImplemented()

def methodtwo(self):
    raise NotImplementedError()

class Son(Parent):
   def methodone(self):
       return 'methodone() is called'

c = Son()
c.methodone()

'methodone () называется'

c.methodtwo()

NotImplementedError

С абстрактным методом

from abc import ABCMeta, abstractmethod

class Parent(metaclass=ABCMeta):
    @abstractmethod
    def methodone(self):
        raise NotImplementedError()
    @abstractmethod
    def methodtwo(self):
        raise NotImplementedError()

class Son(Parent):
    def methodone(self):
        return 'methodone() is called'

c = Son()

TypeError: Невозможно создать экземпляр абстрактного класса Son с помощью абстрактных методов methodtwo.

Так как methodtwo не вызывается в дочернем классе, мы получили ошибку. Правильная реализация ниже

from abc import ABCMeta, abstractmethod

class Parent(metaclass=ABCMeta):
    @abstractmethod
    def methodone(self):
        raise NotImplementedError()
    @abstractmethod
    def methodtwo(self):
        raise NotImplementedError()

class Son(Parent):
    def methodone(self):
        return 'methodone() is called'
    def methodtwo(self):
        return 'methodtwo() is called'

c = Son()
c.methodone()

'methodone () называется'

сим
источник
1
Спасибо. Разве это не то же самое, что было сказано в ответе cerberos выше?
Мухаммед Алкарури