Abstraktes Attribut (keine Eigenschaft)?

78

Was ist die beste Vorgehensweise, um ein abstraktes Instanzattribut zu definieren, jedoch nicht als Eigenschaft?

Ich möchte etwas schreiben wie:

class AbstractFoo(metaclass=ABCMeta):

    @property
    @abstractmethod
    def bar(self):
        pass

class Foo(AbstractFoo):

    def __init__(self):
        self.bar = 3

Anstatt:

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

Eigenschaften sind praktisch, aber für einfache Attribute, die keine Berechnung erfordern, sind sie ein Overkill. Dies ist besonders wichtig für abstrakte Klassen , die vom Benutzer subclassed und umgesetzt werden (ich will nicht , dass jemand zu verwenden zwingen , @propertywenn er gerade geschrieben haben könnte self.foo = fooin der __init__).

Abstrakte Attribute in der Python- Frage werden als einzige zu verwendende Antwort vorgeschlagen @propertyund @abstractmethod: Meine Frage wird nicht beantwortet.

Das ActiveState-Rezept für ein abstraktes Klassenattribut über ist AbstractAttributemöglicherweise der richtige Weg, aber ich bin mir nicht sicher. Es funktioniert auch nur mit Klassenattributen und nicht mit Instanzattributen.

Lapinot
quelle
Warum müssen Sie jemanden zwingen, ein bestimmtes Attribut für seine Klasse zu haben?
Philip Adler
15
Ist es nicht die ganze Sache von ABC? Wenn Sie mein konkretes Beispiel wollen, möchte ich, dass Leute eine Klasse für ihren Sensor schreiben und die Klasse sollte ein self.port-Attribut haben.
Lapinot
Nach dem Nachdenken, ja, ich nehme an, es ist; obwohl ich denke, dass dies eher angesichts der Ententyping fliegt ...
Philip Adler
Vielleicht frage ich nur nach zu vielen Komplikationen, aber es würde mich stören, ABC nicht zu verwenden, wenn ich abstrakte Klassen mache (ich denke, ich werde nur die normale Basisklasse verwenden) ...
Lapinot
Die Lösung von anentropics ist einfach und funktioniert gut. Warum ist es nicht die akzeptierte Antwort?
Dave Kielpinski

Antworten:

29

Wenn Sie wirklich erzwingen möchten, dass eine Unterklasse ein bestimmtes Attribut definiert, können Sie eine Metaklasse verwenden. Persönlich denke ich, dass es übertrieben und nicht sehr pythonisch sein kann, aber Sie könnten so etwas tun:

 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

Grundsätzlich wird die Meta-Klasse neu definiert __call__, um sicherzustellen, dass  check_barsie nach dem Init auf einer Instanz aufgerufen wird.

GoodFoo()  # ok
BadFoo ()  # yield NotImplementedError
Sébastien Deprez
quelle
45

Es ist 2018, wir verdienen eine etwas bessere Lösung:

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

Es wird sich so verhalten:

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

Diese Antwort verwendet den gleichen Ansatz wie die akzeptierte Antwort, lässt sich jedoch gut in das integrierte ABC integrieren und erfordert kein Hilfsprogramm check_bar().

Hier ist der better_abc.pyInhalt:

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

Das Schöne ist, dass Sie Folgendes tun können:

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

und es wird genauso funktionieren wie oben.

Auch kann man verwenden:

class ABC(ABCMeta):
    pass

um einen benutzerdefinierten ABC-Helfer zu definieren. PS. Ich betrachte diesen Code als CC0.

Dies könnte verbessert werden, indem der AST-Parser verwendet wird, um früher (bei Klassendeklaration) durch Scannen des __init__Codes zu erhöhen , aber es scheint vorerst ein Overkill zu sein (es sei denn, jemand ist bereit zu implementieren).

krassowski
quelle
1
Ziemlich ordentlich, das ist in der Tat die Art von Lösung, von der ich vermutete, dass sie existiert (ich denke, ich werde die akzeptierte Antwort ändern). Im Nachhinein ist die Syntax der Deklaration einfach komisch. Auf lange Sicht funktioniert in Python die Ententypisierung: ABC und Anmerkungen lassen den Code nur wie schlechtes Java aussehen (Syntax und Struktur).
Lapinot
3
Ich wünschte, ich könnte dies mehr unterstützen. Hoffentlich wird diese Lösung irgendwann in die Standardbibliothek integriert. Vielen Dank.
BCR
Gibt es einen Grund, warum Sie die DummyAttributeEinstellung erstellt haben, anstatt nur die Einstellung obj = object()im abstract_attributeDekorator?
Andy Perez
1
@AndyPerez, es ist möglicherweise einfacher zu debuggen, wenn Sie ein Objekt der benannten Klasse haben, kein einfaches Objekt, aber ich denke, dass Sie es leicht obj = object()in Ihrem Code ändern können, wenn Sie das bevorzugen.
Krassowski
Danke, @krassowski. Folgen Sie der Frage: Ich habe versucht, dies in einem ABC zu implementieren, habe jedoch Probleme damit, da ich das überschreibe __setattr__, um eine Validierung des angegebenen Werts durchzuführen, bevor das Attribut festgelegt wird. Es wird immer wieder ein Fehler ausgegeben, der besagt, dass das Attribut "schreibgeschützt" ist. Gibt es eine Möglichkeit, dies zu umgehen?
Andy Perez
21

Nur weil Sie es als abstractpropertyabstrakte Basisklasse definieren , müssen Sie keine Eigenschaft für die Unterklasse erstellen.

zB können Sie:

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>

Wenn Sie den Wert in initialisieren möchten, __init__können Sie Folgendes tun:

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>

Da Python 3.3 abstractpropertyist veraltet . Daher sollten Python 3-Benutzer stattdessen Folgendes verwenden:

from abc import ABCMeta, abstractmethod

class X(metaclass=ABCMeta):
    @property
    @abstractmethod
    def required(self):
        raise NotImplementedError
Anentropisch
quelle
Es ist eine Schande, dass die ABC-Prüfung nicht bei der Initialisierung der Instanz und nicht bei der Klassendefinition erfolgt. Auf diese Weise wäre ein abstraktes Attribut möglich. das würde uns erlauben, eine abstractattribute_or_property zu haben - was die op will
chris
Das Hinzufügen eines attr in init funktioniert bei mir nicht. python3.6
zhukovgreen
1
@ArtemZhukov Das obige Beispiel ist eine Python 3.6-Sitzung in IPython, es funktioniert. Sie benötigen jedoch auch den required = NoneKlassenkörper, wie oben gezeigt.
Anentropic
1
@chris, siehe meine stackoverflow.com/a/50381071/6646912 für eine ABC-Unterklasse, die die Überprüfung bei der Initialisierung durchführt.
Krassowski
@krassowski, der nicht mehr als die @abstractpropertyobigen Definitionen erreicht, aber schlimmer ist, weil er keine Attribute behandelt, die im Klassenkörper definiert sind, anstatt__init__
Anentropic
7

Das Problem ist nicht was , sondern wann :

from abc import ABCMeta, abstractmethod

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

class Foo(AbstractFoo):
    bar = object()

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

Es spielt keine Rolle, dass dies barkeine Methode ist! Das Problem ist, dass __subclasshook__die Methode zur Durchführung der Prüfung a ist classmethodund sich nur darum kümmert, ob die Klasse , nicht die Instanz , das Attribut hat.


Ich schlage vor, Sie erzwingen dies einfach nicht, da es ein schwieriges Problem ist. Die Alternative besteht darin, sie zu zwingen, das Attribut vorab zu definieren, aber das lässt nur Dummy-Attribute übrig, die nur Fehler zum Schweigen bringen.

Veedrac
quelle
Ja, das habe ich seitdem gemacht (aber es ist seltsam, es als abstrakte Methode zu definieren ). Die Frage nach dem Instanzattribut wird jedoch nicht gelöst.
Lapinot
Ich denke, dies ist eine noch bessere (und pythonischere) Lösung.
Kawing-Chiu
3

Wie Anentropic sagte, müssen Sie ein nicht abstractpropertyals ein anderes implementieren property.

Eine Sache, die alle Antworten zu vernachlässigen scheinen, sind die Mitglieder-Slots von Python (das __slots__Klassenattribut). Benutzer Ihrer ABCs, die zur Implementierung abstrakter Eigenschaften erforderlich sind, können diese einfach innerhalb definieren, __slots__wenn nur ein Datenattribut erforderlich ist.

Also mit so etwas wie

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

    bar = abc.abstractproperty()

Benutzer können Unterklassen einfach wie folgt definieren:

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

    # define Foo as desired

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

Hier Foo.barverhält es sich wie ein reguläres Instanzattribut, das nur anders implementiert ist. Dies ist einfach, effizient und vermeidet die von @propertyIhnen beschriebene Boilerplate.

Dies funktioniert unabhängig davon, ob ABCs __slots__an den Körpern ihrer Klasse definiert sind oder nicht . Wenn Sie den __slots__gesamten Weg gehen, sparen Sie nicht nur Speicher und bieten schnellere Attributzugriffe, sondern auch einen aussagekräftigen Deskriptor, anstatt Zwischenprodukte (z. B. bar = Noneoder ähnliches) in Unterklassen zu haben. 1

Einige Antworten schlagen vor, die "abstrakte" Attributprüfung nach der Instanziierung durchzuführen (dh bei der Metaklassenmethode __call__()), aber ich finde, dass dies nicht nur verschwenderisch, sondern auch potenziell ineffizient ist, da der Initialisierungsschritt zeitaufwändig sein kann.

Kurz gesagt, für Unterklassen von ABCs ist es erforderlich, den relevanten Deskriptor (sei es eine Eigenschaft oder eine Methode) zu überschreiben, egal wie, und Ihren Benutzern zu dokumentieren, dass es möglich ist, ihn __slots__als Implementierung für abstrakte Eigenschaften zu verwenden für mich als angemessener Ansatz.


1 Zumindest sollten ABCs in jedem Fall immer ein leeres __slots__Klassenattribut definieren , da Unterklassen sonst gezwungen sind, __dict__(dynamischen Attributzugriff) und __weakref__(schwache Referenzunterstützung) zu haben, wenn sie instanziiert werden. Siehe abcoder collections.abcModule für Beispiele dies der Fall innerhalb der Standardbibliothek zu sein.

Edward Elrick
quelle
0

Ich habe eine Weile danach gesucht, aber nichts gesehen, was mir gefällt. Wie Sie wahrscheinlich wissen, wenn Sie:

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()

Sie bekommen eine, AttributeError: can't set attributedie nervt.

Um dies zu umgehen, können Sie Folgendes tun:

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

Dies vermeidet das, AttributeError: can't set attributeaber wenn Sie die abstrakte Eigenschaft einfach alle zusammen weglassen:

class AbstractFoo(object):
    pass

class Foo(AbstractFoo):
    pass

f = Foo()
f.bar

Sie erhalten eine, AttributeError: 'Foo' object has no attribute 'bar'die wohl fast so gut ist wie der NotImplementedError. Meine Lösung besteht also darin, nur eine Fehlermeldung von einer anderen zu tauschen. Und Sie müssen self._bar anstelle von self.bar in der Init verwenden .

James Irwin
quelle
Zunächst möchte ich erwähnen, dass Sie Ihre Klasse trotz der Benennung Ihrer Klasse AbstractFoonicht abstrakt gemacht haben und diese Antwort völlig unabhängig von dem von OP festgelegten Kontext ist. Das heißt, Ihre Problemumgehung für das Problem "nicht einstellbares Attribut" der regulären Eigenschaften kann abhängig von der Häufigkeit des Zugriffs auf das Attribut etwas teuer sein. Ein billigerer (basierend auf Ihrem ersten Codeblock) ist dies in Ihrem Foo.__init__():self.__dict__['bar'] = "bar"
Mikenerone
0

Wenn Sie https://docs.python.org/2/library/abc.html folgen, können Sie in Python 2.7 Folgendes tun:

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())
Javier Montón
quelle