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


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

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

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

5 155

5 ответов:

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

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

текст

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

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

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

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

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

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

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

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

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

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

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

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

объектная модель 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

к сожалению, это одна из тех ловушек" просто не делайте этого " (из которых Питон имеет относительно мало!): не определять Азбука с __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 способ вызова и использования абстрактного. Программа написана на 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 () называется'