Зачем использовать абстрактные базовые классы в Python? - PullRequest
184 голосов
/ 26 августа 2010

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

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

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

Ответы [ 5 ]

203 голосов
/ 12 октября 2013

@ Oddthinking ответ не является неправильным, но я думаю, что он пропускает real , практическую причину, по которой 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 как __subclasshook__, так и неабстрактными методами. Более того, вы должны привести свое определение __subclasshook__ в соответствие с набором абстрактных методов, которые определяет ваша ABC.

141 голосов
/ 26 августа 2010

Короткая версия

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

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

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

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

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

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

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

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

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

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

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

92 голосов
/ 19 мая 2015

Удобной особенностью 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

16 голосов
/ 26 августа 2010

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

5 голосов
/ 19 июля 2018

Абстрактный метод, убедитесь, что любой метод, который вы вызываете в родительском классе, должен присутствовать в дочернем классе. Ниже приведены способы вызова 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 не вызывается в дочернем классе, мы получили ошибку. Правильная реализация ниже

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 () называется'

...