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

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

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

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

5 ответов


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

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

текст

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

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

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

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

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

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

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

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

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


@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

другими словами, если вы реализуете интерфейс, вы подклассе! ABCs предоставляют формальный способ определения интерфейсов в Python, оставаясь верным духу duck-typing. Кроме того, это работает таким образом, что чтит Принцип Открытости-Закрытости.

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

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

я нахожу, что мне редко нужно писать свою собственную ABC-я предпочитаю полагаться на duck typing - и я обычно обнаруживаю необходимость в одном через рефакторинг. Если я вижу полиморфную функцию, выполняющую множество проверок атрибутов, или множество функций, выполняющих одни и те же проверки атрибутов, этот запах предполагает существование 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 имеет относительно мало!): избегайте определения ABCs с обоими a __subclasshook__ и не абстрактные методы. Кроме того, вы должны сделать свое определение __subclasshook__ в соответствии с набором абстрактных методов, которые определяет ABC.


удобная функция ABCs заключается в том, что если вы не реализуете все необходимые методы (и свойства), вы получаете ошибку при создании экземпляра, а не 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

Edit: чтобы включить синтаксис python3, спасибо @PandasRocks


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


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