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


213

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

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

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

Ответы:


162

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

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

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

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

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

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

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

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

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

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

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

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


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


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 выше?
— Мухаммед Алкарури
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.