Абстрактний атрибут (не властивість)?


81

Яка найкраща практика визначати атрибут абстрактного екземпляра, але не як властивість?

Я хотів би написати щось на зразок:

class AbstractFoo(metaclass=ABCMeta):

    @property
    @abstractmethod
    def bar(self):
        pass

class Foo(AbstractFoo):

    def __init__(self):
        self.bar = 3

Замість:

class Foo(AbstractFoo):

    def __init__(self):
        self._bar = 3

    @property
    def bar(self):
        return self._bar

    @bar.setter
    def setbar(self, bar):
        self._bar = bar

    @bar.deleter
    def delbar(self):
        del self._bar

Властивості зручні, але для простих атрибутів, які не потребують обчислень, вони є надмірними. Це особливо важливо для абстрактних класів, які будуть підкласифіковані та реалізовані користувачем (я не хочу змушувати когось використовувати, @propertyколи він просто міг написати self.foo = fooв __init__).

Абстрактні атрибути у питанні Python пропонуються як єдину відповідь для використання, @propertyі @abstractmethod: він не відповідає на моє запитання.

Рецепт ActiveState для абстрактного атрибута класу через AbstractAttributeможе бути правильним шляхом, але я не впевнений. Він також працює лише з атрибутами класу, а не з атрибутами екземпляра.


Чому вам потрібно змушувати когось мати певний атрибут у своєму класі?
Філіп Адлер

17
Чи не вся справа в ABC? Якщо вам потрібен мій конкретний приклад, я хочу, щоб люди писали клас для свого датчика, і клас повинен мати атрибут self.port.
Лапінот

Подумавши, так, я гадаю, що так; хоча я думаю, що це скоріше летить перед обличчям каченят ...
Філіп Адлер

Можливо, я просто прошу занадто багато ускладнень, але мені
заважало

Рішення anentropics просте і добре працює. Чому це не прийнята відповідь?
Dave Kielpinski

Відповіді:


29

Якщо ви дійсно хочете забезпечити, щоб підклас визначав даний атрибут, ви можете використовувати метаклас. Особисто я думаю, що це може бути надмірним і не надто пітонічним, але ви можете зробити щось подібне:

 class AbstractFooMeta(type):

     def __call__(cls, *args, **kwargs):
         """Called when you call Foo(*args, **kwargs) """
         obj = type.__call__(cls, *args, **kwargs)
         obj.check_bar()
         return obj


 class AbstractFoo(object):
     __metaclass__ = AbstractFooMeta
     bar = None

     def check_bar(self):
         if self.bar is None:
             raise NotImplementedError('Subclasses must define bar')


 class GoodFoo(AbstractFoo):
     def __init__(self):
         self.bar = 3


 class BadFoo(AbstractFoo):
     def __init__(self):
         pass

В основному мета-клас перевизначає, __call__щоб переконатися, що  check_barйого викликають після ініціювання на екземплярі.

GoodFoo()  # ok
BadFoo ()  # yield NotImplementedError

50

Це 2018 рік, ми заслуговуємо трохи кращого рішення:

from better_abc import ABCMeta, abstract_attribute    # see below

class AbstractFoo(metaclass=ABCMeta):

    @abstract_attribute
    def bar(self):
        pass

class Foo(AbstractFoo):
    def __init__(self):
        self.bar = 3

class BadFoo(AbstractFoo):
    def __init__(self):
        pass

Він буде поводитися так:

Foo()     # ok
BadFoo()  # will raise: NotImplementedError: Can't instantiate abstract class BadFoo
# with abstract attributes: bar

Ця відповідь використовує той самий підхід, що і прийнята відповідь, але добре інтегрується із вбудованою АВС і не вимагає зразків check_bar()помічників.

Ось better_abc.pyзміст:

from abc import ABCMeta as NativeABCMeta

class DummyAttribute:
    pass

def abstract_attribute(obj=None):
    if obj is None:
        obj = DummyAttribute()
    obj.__is_abstract_attribute__ = True
    return obj


class ABCMeta(NativeABCMeta):

    def __call__(cls, *args, **kwargs):
        instance = NativeABCMeta.__call__(cls, *args, **kwargs)
        abstract_attributes = {
            name
            for name in dir(instance)
            if getattr(getattr(instance, name), '__is_abstract_attribute__', False)
        }
        if abstract_attributes:
            raise NotImplementedError(
                "Can't instantiate abstract class {} with"
                " abstract attributes: {}".format(
                    cls.__name__,
                    ', '.join(abstract_attributes)
                )
            )
        return instance

Найприємніше, що ви можете:

class AbstractFoo(metaclass=ABCMeta):
    bar = abstract_attribute()

і він буде працювати так само, як і вище.

Також можна використовувати:

class ABC(ABCMeta):
    pass

для визначення власного помічника ABC. PS. Я вважаю цей код CC0.

Це можна було б покращити, використовуючи синтаксичний аналізатор AST для підвищення раніше (при оголошенні класу), скануючи __init__код, але це, здається, на сьогодні надмірно (якщо хтось не бажає реалізувати).


1
Досить акуратний, це справді таке рішення, про яке я підозрював, що існує (я думаю, я збираюся змінити прийняту відповідь). З огляду на минуле, синтаксис оголошення просто дуже дивний. У довгостроковій перспективі те, що працює в python, - це набір качок: ABC та анотації просто зроблять код схожим на погану java (синтаксис та структура).
Лапінот,

3
Я хотів би, щоб я підтримав це більше. Сподіваємось, це рішення колись буде інтегровано до стандартної бібліотеки. Дякую.
BCR

Чи є причина , чому ви створили DummyAttributeзамість того , щоб просто встановивши obj = object()в abstract_attributeДекораторі?
Andy Perez

1
@AndyPerez, може бути простіше налагодити, якщо у вас є об'єкт з іменованим класом, а не звичайний об'єкт, але я думаю, що ви можете легко змінити його на obj = object()свій код, якщо вам це подобається.
krassowski

Дякую, @krassowski. Виконайте наступне запитання: Я спробував реалізувати це в ABC, але у мене проблеми з цим, оскільки я замінюю значення, __setattr__щоб виконати перевірку наданого значення перед встановленням атрибута. Постійно виникає помилка, що атрибут "лише для читання". Чи є спосіб обійти це?
Енді Перес,

23

Те, що ви визначаєте його як abstractpropertyабстрактний базовий клас, не означає, що вам потрібно створити властивість для підкласу.

наприклад, ви можете:

In [1]: from abc import ABCMeta, abstractproperty

In [2]: class X(metaclass=ABCMeta):
   ...:     @abstractproperty
   ...:     def required(self):
   ...:         raise NotImplementedError
   ...:

In [3]: class Y(X):
   ...:     required = True
   ...:

In [4]: Y()
Out[4]: <__main__.Y at 0x10ae0d390>

Якщо ви хочете ініціалізувати значення в, __init__ви можете зробити це:

In [5]: class Z(X):
   ...:     required = None
   ...:     def __init__(self, value):
   ...:         self.required = value
   ...:

In [6]: Z(value=3)
Out[6]: <__main__.Z at 0x10ae15a20>

Оскільки Python 3.3 abstractpropertyє застарілим . Тож користувачі Python 3 повинні використовувати замість цього:

from abc import ABCMeta, abstractmethod

class X(metaclass=ABCMeta):
    @property
    @abstractmethod
    def required(self):
        raise NotImplementedError

соромно, що перевірка ABC не відбувається при ініціалізації екземпляра, а не при визначенні класу, таким чином, abstractattribute буде можливим. що дозволило б нам мати abstractattribute_or_property - саме цього хоче оператор
Кріс

додавання attr у init для мене не працює. python3.6
zhukovgreen

1
@ArtemZhukov наведений вище приклад - це сеанс Python 3.6 в IPython, він працює. Вам також потрібно required = Noneтіло в класі, як показано вище.
Anentropic

1
@ Кріс, см мій stackoverflow.com/a/50381071/6646912 для підкласу ABC , який робить перевірку при ініціалізації.
krassowski

@krassowski, який не досягає нічого більшого, ніж @abstractpropertyописані вище, але гірший, оскільки не обробляє атрибути, визначені в тілі класу, а не__init__
Anentropic

7

Проблема не в тому , а в тому, коли :

from abc import ABCMeta, abstractmethod

class AbstractFoo(metaclass=ABCMeta):
    @abstractmethod
    def bar():
        pass

class Foo(AbstractFoo):
    bar = object()

isinstance(Foo(), AbstractFoo)
#>>> True

Неважливо, що barце не метод! Проблема полягає в тому __subclasshook__, що метод здійснення перевірки є a classmethod, тому лише цікавить, чи має клас , а не екземпляр , атрибут.


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


Так, цим я займався з цього часу (але дивно визначати це як абстрактний метод ). Але це не вирішує питання про атрибут екземпляра.
Лапінот

Я думаю, що це ще краще (і більш пітонічне) рішення.
kawing-chiu

3

Як сказав Анентропік , вам не потрібно застосовувати abstractpropertyяк інший property.

Однак одне, що всі відповіді нехтують, - це слоти-члени Python ( __slots__атрибут class). Користувачі ваших ABC, яким потрібно реалізувати абстрактні властивості, можуть просто визначити їх всередині__slots__ якщо все, що потрібно - це атрибут data.

Отже, з чимось на зразок,

class AbstractFoo(abc.ABC):
    __slots__ = ()

    bar = abc.abstractproperty()

Користувачі можуть визначати підкласи просто так,

class Foo(AbstractFoo):
    __slots__ = 'bar',  # the only requirement

    # define Foo as desired

    def __init__(self):
        self.bar = ...

Тут Foo.barповодиться як звичайний атрибут екземпляра, який він є, просто реалізований інакше. Це просто, ефективно та дозволяє уникнути @propertyопису, який ви описали.

Це працює незалежно від того, визначають АВС __slots__в тілах свого класу чи ні . Однак, якщо пройти __slots__весь шлях, це не лише економить пам’ять та забезпечує швидший доступ до атрибутів, але також дає значущий дескриптор замість того, щоб мати проміжні продукти (наприклад, bar = Noneабо подібні) у підкласах. 1

Кілька відповідей пропонують провести перевірку атрибутів "abstract" після створення (наприклад, у мета-класі__call__() методом ), але я вважаю, що не лише марнотратним, але й потенційно неефективним, оскільки крок ініціалізації може зайняти багато часу.

Коротше кажучи, для підкласів ABC потрібно перевизначити відповідний дескриптор (будь то властивість чи метод), не має значення, як, і __slots__здається документування вашим користувачам, що це можна використовувати як реалізацію для абстрактних властивостей для мене як більш адекватний підхід.


1 У будь-якому випадку, принаймні, ABC завжди повинні визначати порожній __slots__атрибут класу, оскільки в іншому випадку підкласи примусово мають __dict__(динамічний доступ до атрибутів) та __weakref__(слабка підтримка посилань), коли створюються екземпляри. Див. Модулі abcабо collections.abcдля прикладів цього у стандартній бібліотеці.


0

Я шукав це довгий час, але не бачив нічого, що мені подобається. Як ви, мабуть, знаєте:

class AbstractFoo(object):
    @property
    def bar(self):
        raise NotImplementedError(
                "Subclasses of AbstractFoo must set an instance attribute "
                "self._bar in it's __init__ method")

class Foo(AbstractFoo):
    def __init__(self):
        self.bar = "bar"

f = Foo()

Ви отримуєте, AttributeError: can't set attributeщо дратує.

Щоб обійти це, ви можете зробити:

class AbstractFoo(object):

    @property
    def bar(self):
        try:
            return self._bar
        except AttributeError:
            raise NotImplementedError(
                "Subclasses of AbstractFoo must set an instance attribute "
                "self._bar in it's __init__ method")

class OkFoo(AbstractFoo):
    def __init__(self):
        self._bar = 3

class BadFoo(AbstractFoo):
    pass

a = OkFoo()
b = BadFoo()
print a.bar
print b.bar  # raises a NotImplementedError

Це дозволяє уникнути, AttributeError: can't set attributeале якщо ви просто залишите абстрактне властивість разом:

class AbstractFoo(object):
    pass

class Foo(AbstractFoo):
    pass

f = Foo()
f.bar

Ви отримуєте AttributeError: 'Foo' object has no attribute 'bar'який, можливо, майже такий же хороший, як NotImplementedError. Отже, насправді моє рішення - це просто обмін одними повідомленнями про помилки з іншого .. і вам потрібно використовувати self._bar, а не self.bar в init .


По-перше, я хочу зазначити, що, незважаючи на назву вашого класу AbstractFoo, ви насправді не зробили свій клас абстрактним, і ця відповідь абсолютно не пов’язана з контекстом, викладеним OP. Тим не менш, ваш спосіб вирішення проблеми "невстановлюваних атрибутів" звичайних властивостей може бути дорогим, залежно від того, як часто доступ до атрибута доступний. Більш дешевий (на основі вашого першого блоку коду) це у вашому Foo.__init__():self.__dict__['bar'] = "bar"
mikenerone

0

Слідуючи https://docs.python.org/2/library/abc.html, ви можете зробити щось подібне в Python 2.7:

from abc import ABCMeta, abstractproperty


class Test(object):
    __metaclass__ = ABCMeta

    @abstractproperty
    def test(self): yield None

    def get_test(self):
        return self.test


class TestChild(Test):

    test = None

    def __init__(self, var):
        self.test = var


a = TestChild('test')
print(a.get_test())
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.