Навіщо використовувати абстрактні базові класи в Python?


213

Оскільки я звик до старих способів введення качок в Python, я не розумію потреби в ABC (абстрактні базові класи). Допомога добре про те , як використовувати їх.

Я спробував прочитати обґрунтування в ПЕП , але це перейшло мені над головою. Якщо я шукав контейнер послідовних змін , я перевірив би __setitem__, чи скоріше спробував би його використовувати ( EAFP ). Я не натрапив на використання в реальному житті модуля чисел , який використовує ABC, але це найбільш близьке для мене розуміння.

Хтось може мені пояснити обґрунтування, будь ласка?

Відповіді:


162

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

ABC пропонують більш високий рівень семантичного контракту між клієнтами та реалізованими класами.

Довга версія

Існує контракт між класом та його абонентами. Клас обіцяє робити певні речі і мати певні властивості.

У контракту є різні рівні.

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

Статично типізованою мовою цей договір фактично буде виконаний компілятором. У 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.
Ehtesh Choudhury

1
Думаю, ця стаття це дуже добре пояснює: dbader.org/blog/ab абстракт
base-

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__атрибутом вважається підкласом Container, навіть якщо він не підкласифікує його безпосередньо. Тож я можу це написати:

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

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

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

Об'єктна модель Python виглядає поверхнево подібною до моделі більш "традиційної" OO (під якою я маю на увазі Java *) - у нас є класи yer, yer-об'єкти, yer-методи - але коли ви подряпаєте поверхню, ви знайдете щось набагато багатше і більш гнучким. Так само, поняття 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
Ви повинні змінити форматування з курсиву на жирне. "якщо ви реалізуєте правильний інтерфейс, ви підклас!" Дуже стисле пояснення. Дякую!
marti

108

Зручна особливість Азбуки полягає в тому, що якщо ви не реалізуєте всі необхідні методи (та властивості), ви отримаєте помилку при інстанціюванні, а не 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/ab абстракт-base-classes-in- python

Редагувати: включити синтаксис python3, спасибі @PandasRocks


5
Приклад та посилання були дуже корисними. Дякую!
nalyd88

якщо Base визначено в окремому файлі, ви повинні успадкувати його як Base.Base або змінити рядок імпорту на 'from Base import Base'
Ryan Tennill

Слід зазначити, що в Python3 синтаксис трохи інший. Дивіться цей відповідь: stackoverflow.com/questions/28688784 / ...
PandasRocks

7
Виходячи з C # фону, це причина використовувати абстрактні класи. Ви надаєте функціональність, але заявляєте, що функціональність потребує подальшої реалізації. Інші відповіді, здається, пропускають це питання.
Джош Ное

18

Це дозволить визначити, чи підтримує об'єкт даний протокол, не перевіряючи наявність усіх методів у протоколі або не викликаючи виключення глибоко на «ворожій» території через непідтримку набагато простіше.


6

Абстрактний метод переконайтеся, що будь-який метод, який ви викликаєте в батьківському класі, повинен бути відображений у дочірньому класі. Нижче наведено норалль спосіб дзвінка та використання абстрактних. Програма написана в 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()

NotImPLmentedError

З абстрактним методом

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 () називається'


1
Дякую. Хіба це не те саме, що було зроблено у відповіді cerberos вище?
Мухаммед Алькарурі
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.