Abstrakte Attribute in Python [Duplikat]

90

Was ist der kürzeste / eleganteste Weg, um den folgenden Scala-Code mit einem abstrakten Attribut in Python zu implementieren?

abstract class Controller {

    val path: String

}

Eine Unterklasse von Controllerwird vom Scala-Compiler erzwungen, um "Pfad" zu definieren. Eine Unterklasse würde folgendermaßen aussehen:

class MyController extends Controller {

    override val path = "/home"

}
Deamon
quelle
1
Was hast du versucht? Bitte posten Sie Ihren Python-Code bei Problemen oder Fragen zu Ihrer Lösung.
S.Lott
"Eine Unterklasse von Controller wird vom Scala-Compiler gezwungen," Pfad "zu definieren." ... wann erzwungen? Wenn es Zeit zum Kompilieren ist, haben Sie kein Glück. Wenn es Laufzeit ist, wie genau soll es dann "erzwungen" werden? Mit anderen Worten, gibt es einen Unterschied zwischen dem Auslösen eines AttributeError und eines NotImplementedError? Warum?
Detly
2
Ich weiß, dass Python eine dynamische Sprache ist und dass der Python-Interpreter keine statischen Typen erzwingen kann. Es ist mir wichtig, dass es so früh wie möglich versagt und dass es leicht ist, den Ort zu finden, an dem der Fehler aufgetreten ist und warum.
Deamon
Siehe auch
guettli
Es gibt einige relevante Antworten auf ein neueres Duplikat: stackoverflow.com/questions/23831510/… , aber im Grunde genommen gibt es ab Python 3.8 keine gute Lösung.
Janus

Antworten:

92

Python hat hierfür eine integrierte Ausnahme, die jedoch erst zur Laufzeit auftritt.

class Base(object):
    @property
    def path(self):
        raise NotImplementedError


class SubClass(Base):
    path = 'blah'

quelle
13
Insbesondere wird die Ausnahme erst auftreten, wenn auf das Attribut zugegriffen wird. In diesem Fall hätten Sie ohnehin einen AttributeError erhalten.
Ben James
5
Ich denke, dass das Erhöhen von a NotImplementedErrorexpliziter und daher wahrscheinlich besser ist, als es einem zu überlassen AttributeError.
Blokeley
Sie können auch eine Nachricht wie "Die abstrakte Klassenbasis kann nicht instanziiert werden" hinzufügen, wenn Sie selbst eine Ausnahme auslösen.
Bastien Léonard
12
Nicht, dass dies nur funktioniert, wenn pathes direkt auf dem eingestellt ist SubClass. Wenn Sie in einer bestimmten Instanz sc = SubClass()versuchen, sc.path = 'blah'eine Methode festzulegen oder zu verwenden, die so etwas enthält, self.path = 'blah'ohne pathdirekt zu definieren SubClass, erhalten Sie eine AttributeError: can't set attribute.
Erik
3
Es ist nicht die Antwort !! Dies ist kein Instanzfeld. Die in der Klasse abgelegte Scala ist ein Instanzfeld, da Scala die statische und die Instanz trennt.
WeiChing
100

Python 3.3+

from abc import ABCMeta, abstractmethod


class A(metaclass=ABCMeta):
    def __init__(self):
        # ...
        pass

    @property
    @abstractmethod
    def a(self):
        pass

    @abstractmethod
    def b(self):
        pass


class B(A):
    a = 1

    def b(self):
        pass

Wenn Sie nicht deklarieren aoder bin der abgeleiteten Klasse sind, Bwird Folgendes TypeErrorausgelöst:

TypeError: Abstrakte Klasse kann nicht Bmit abstrakten Methoden instanziiert werdena

Python 2.7

Hierfür gibt es einen @ abstractproperty- Dekorateur:

from abc import ABCMeta, abstractmethod, abstractproperty


class A:
    __metaclass__ = ABCMeta

    def __init__(self):
        # ...
        pass

    @abstractproperty
    def a(self):
        pass

    @abstractmethod
    def b(self):
        pass


class B(A):
    a = 1

    def b(self):
        pass
Wtower
quelle
Meine "A" -Klasse ist eine Unterklasse von Exception, und dies scheint das Beispiel zu brechen.
Chris2048
1
@ Chris2048 wie bricht es es? Welchen Fehler bekommst du?
Joel
Gibt es eine Möglichkeit, dies auf eine Python 2/3-agnostische Weise zu schreiben, die dieser Frage ähnelt ?
bluenote10
7
Ihre Python 3-Lösung wird aals Klasseneigenschaft von Banstelle einer Objekteigenschaft implementiert . Wenn Sie stattdessen `` `Klasse B (A): def __init __ (self): self.a = 1 ...` `` ausführen, wird beim Instanziieren ein Fehler angezeigt B: "Die abstrakte Klasse B kann nicht mit abstrakten Methoden a instanziiert werden."
Janus
@ Janus macht einen wichtigen Punkt ... in der Lage zu sein class MyController(val path: String) extends Controllerist eine wertvolle Option in der Scala-Version verfügbar, fehlt aber hier
Joel
46

Seit diese Frage ursprünglich gestellt wurde, hat Python die Implementierung abstrakter Klassen geändert. Ich habe einen etwas anderen Ansatz verwendet, indem ich den abc.ABC-Formalismus in Python 3.6 verwendet habe. Hier definiere ich die Konstante als eine Eigenschaft, die in jeder Unterklasse definiert werden muss.

from abc import ABC, abstractmethod


class Base(ABC):

    @property
    @classmethod
    @abstractmethod
    def CONSTANT(cls):
        return NotImplementedError

    def print_constant(self):
        print(type(self).CONSTANT)


class Derived(Base):
    CONSTANT = 42

Dadurch wird die abgeleitete Klasse gezwungen, die Konstante zu definieren. Andernfalls wird eine TypeErrorAusnahme ausgelöst, wenn Sie versuchen, die Unterklasse zu instanziieren. Wenn Sie die Konstante für eine in der abstrakten Klasse implementierte Funktionalität verwenden möchten, müssen Sie über auf die Unterklassenkonstante zugreifentype(self).CONSTANT statt nurCONSTANT , da der Wert in der Basisklasse undefiniert ist.

Es gibt andere Möglichkeiten, dies zu implementieren, aber ich mag diese Syntax, da sie mir für den Leser am einfachsten und offensichtlichsten erscheint.

Die vorherigen Antworten haben alle nützliche Punkte berührt, aber ich bin der Meinung, dass die akzeptierte Antwort die Frage nicht direkt beantwortet, weil

  • Die Frage fordert die Implementierung in einer abstrakten Klasse, aber die akzeptierte Antwort folgt nicht dem abstrakten Formalismus.
  • Die Frage lautet, ob die Implementierung erzwungen wird. Ich würde argumentieren, dass die Durchsetzung in dieser Antwort strenger ist, da sie einen Laufzeitfehler verursacht, wenn die Unterklasse instanziiert wird, wenn sie CONSTANTnicht definiert ist. Die akzeptierte Antwort ermöglicht die Instanziierung des Objekts und gibt nur beim CONSTANTZugriff einen Fehler aus , wodurch die Durchsetzung weniger streng wird.

Dies ist kein Fehler bei den ursprünglichen Antworten. Seit der Veröffentlichung wurden wesentliche Änderungen an der Syntax der abstrakten Klasse vorgenommen, die in diesem Fall eine übersichtlichere und funktionalere Implementierung ermöglichen.

James
quelle
1
Dies scheint die einzig richtige Antwort zu sein, die auch beschwichtigtmypy
Anthony Sottile
1
Vielen Dank für eine der wenigen Antworten, die sich tatsächlich mit der Frage befassen. Kann ich fragen, was der Zweck der print_constantMethode ist? Soweit ich das beurteilen kann, macht es nichts?
Brian Joseph
1
Hier können Sie es nicht aufrufen, ohne die abgeleitete Klasse zu instanziieren. Also kein Unterschied, wenn Sie @classmethod Decorator
zhukovgreen
1
@BrianJoseph - Die print_constantMethode soll veranschaulichen, wie auf die Konstante in der übergeordneten Klasse zugegriffen wird, da sie nur in der untergeordneten Klasse definiert ist
James
1
Wenn Sie der Definition von eine Rückgabetypdeklaration hinzufügen, Base.CONSTANTüberprüft mypy auch, ob die Unterklasse den richtigen Typ für die Definition von verwendet CONSTANT.
Florian Brucker
23

In Python 3.6+ können Sie ein Attribut einer abstrakten Klasse (oder einer beliebigen Variablen) mit Anmerkungen versehen, ohne einen Wert für dieses Attribut anzugeben .

class Controller:
    path: str

class MyController(Controller):
    path = "/home"

Dies führt zu sehr sauberem Code, bei dem es offensichtlich ist, dass das Attribut abstrakt ist. Code, der versucht, auf das Attribut zuzugreifen, wenn er nicht überschrieben wurde, löst einen aus AttributeError.

drhagen
quelle
20
c = Controller () funktioniert immer noch. Die Controller-Klasse ist nicht abstrakt.
Isilpekel
16

Sie können ein Attribut in der abstrakten Basisklasse abc.ABC mit einem Wert erstellen, z. B. NotImplementedwenn das Attribut nicht überschrieben und dann verwendet wird, wird zur Laufzeit ein Fehler angezeigt.

Der folgende Code verwendet einen PEP 484- Typhinweis, um PyCharm dabei zu helfen, den Typ des pathAttributs auch statisch korrekt zu analysieren .

import abc


class Controller(abc.ABC):
    path: str = NotImplemented

class MyController(Controller):
    path = "/home"
Phönix
quelle
1
Beachten Sie, dass dies kein Instanzfeld ist. Das Scala-Feld ist immer ein Instanzmitglied in der Klassendefinition. Weil Scala die Statik und die Instanz trennt.
WeiChing
11

Für Python 3.3 + gibt es eine elegante Lösung

from abc import ABC, abstractmethod

class BaseController(ABC):
    @property
    @abstractmethod
    def path(self) -> str:
        ...

class Controller(BaseController):
    path = "/home"


# Instead of an elipsis, you can add a docstring,
# This approach provides more clarity
class AnotherBaseController(ABC):
    @property
    @abstractmethod
    def path(self) -> str:
    """
    :return: the url path of this controller
    """

Wie unterscheidet es sich von anderen Antworten:

  1. ...in einer abstrakten Methode ist der Körper vorzuziehen als pass. Im Gegensatz zu pass, ...impliziert keine Operationen , wo passnur das Fehlen einer tatsächlichen Implementierung bedeutet ,

  2. ...ist mehr zu empfehlen als zu werfen NotImplementedError(...). Dies führt automatisch zu einem äußerst ausführlichen Fehler, wenn die Implementierung eines abstrakten Felds in einer Unterklasse fehlt. Im Gegensatz dazu NotImplementedErrorsagt selbst nicht, warum die Implementierung fehlt. Darüber hinaus erfordert es Handarbeit, um es tatsächlich anzuheben.

Sergei Voitovich
quelle
1
Ihr Punkt 2 ist nicht korrekt. Unabhängig davon, ob Sie tippen ...oder erhöhen NotImplementedErroroder einfach passkeinen Einfluss auf den Fehler haben, der beim Instanziieren einer abstrakten Klasse (oder Unterklasse) mit nicht implementierten Methoden auftritt.
Jephron
Wenn Sie NotImplementedError auslösen, können Sie eine Zeichenfolgennachricht übergeben, um dem Aufrufer detailliertere Informationen darüber zu geben, warum der Fehler ausgelöst wird.
DtechNet
Anstelle von passoder einer elipsis ( ...) ist es besser, mindestens eine Dokumentzeichenfolge für die abstrakte Methode zu deklarieren. Pylint hat mir das gesagt.
Wurst
Danke sausix, das ist ein guter Punkt. Ich werde es in die Antwort aufnehmen
Sergei Voitovich
6

Ab Python 3.6 können Sie __init_subclass__bei der Initialisierung nach den Klassenvariablen der untergeordneten Klasse suchen:

from abc import ABC

class A(ABC):
    @classmethod
    def __init_subclass__(cls):
        required_class_variables = [
            "foo",
            "bar"
        ]
        for var in required_class_variables:
            if not hasattr(cls, var):
                raise NotImplementedError(
                    f'Class {cls} lacks required `{var}` class attribute'
                )

Dies führt zu einem Fehler bei der Initialisierung der untergeordneten Klasse, wenn die fehlende Klassenvariable nicht definiert ist, sodass Sie nicht warten müssen, bis auf die fehlende Klassenvariable zugegriffen wird.

jonathan.scholbach
quelle
__init_subclass__der übergeordneten Klasse wird aufgerufen, bevor die Unterklasse initialisiert wird. Wenn Sie also nach Eigenschaften suchen möchten, die in den Unterklassen festgelegt sind __init__, funktioniert dies nicht.
Notnami
1
@notnami Das ist wahr. In der Frage werden jedoch Klassenvariablen abgefragt, nicht beispielsweise Variablen, die während der __init__Methode festgelegt werden.
jonathan.scholbach
1
Hum ... Bin ich es oder bist du der einzige, der die Frage streng beantwortet? :) +1.
keepAlive
5

Ihre Basisklasse könnte eine __new__Methode implementieren , die nach Klassenattributen sucht:

class Controller(object):
    def __new__(cls, *args, **kargs):
        if not hasattr(cls,'path'): 
            raise NotImplementedError("'Controller' subclasses should have a 'path' attribute")
        return object.__new__(cls)

class C1(Controller):
    path = 42

class C2(Controller):
    pass


c1 = C1() 
# ok

c2 = C2()  
# NotImplementedError: 'Controller' subclasses should have a 'path' attribute

Auf diese Weise wird der Fehler bei der Instanziierung ausgelöst

Juh_
quelle
Interessanterweise funktioniert dies nicht, wenn Sie def __init __ (self) verwenden müssen.
szeitlin
3

Die Implementierung von Python3.6 könnte folgendermaßen aussehen:

In [20]: class X:
    ...:     def __init_subclass__(cls):
    ...:         if not hasattr(cls, 'required'):
    ...:             raise NotImplementedError

In [21]: class Y(X):
    ...:     required = 5
    ...:     

In [22]: Y()
Out[22]: <__main__.Y at 0x7f08408c9a20>
zhukovgreen
quelle
Ich bin mir nicht sicher, ob dies einen Typfehler auslöst, wenn Ihre Unterklassen das Attribut nicht implementieren, falls Sie die Typprüfung von einer IDE verwenden
Allen Wang
3

Ich habe nur ein bisschen @ James Antwort geändert , damit all diese Dekorateure nicht so viel Platz einnehmen. Wenn Sie mehrere solche abstrakten Eigenschaften definieren mussten, ist dies praktisch:

from abc import ABC, abstractmethod

def abstractproperty(func):
   return property(classmethod(abstractmethod(func)))

class Base(ABC):

    @abstractproperty
    def CONSTANT(cls): ...

    def print_constant(self):
        print(type(self).CONSTANT)


class Derived(Base):
    CONSTANT = 42

class BadDerived(Base):
    BAD_CONSTANT = 42

Derived()       # -> Fine
BadDerived()    # -> Error

dankal444
quelle
1

Schauen Sie sich das Modul abc (Abtract Base Class) an: http://docs.python.org/library/abc.html

Meiner Meinung nach besteht die einfachste und häufigste Lösung darin, eine Ausnahme auszulösen, wenn eine Instanz der Basisklasse erstellt wird oder wenn auf ihre Eigenschaft zugegriffen wird.

Bastien Léonard
quelle
12
Bitte erläutern Sie: Wie hilft das abc-Modul in diesem Zusammenhang?
Guettli
1

Bastien Léonards Antwort erwähnt das abstrakte Basisklassenmodul und Brendan Abels Antwort befasst sich mit nicht implementierten Attributen, die Fehler verursachen. Um sicherzustellen, dass die Klasse nicht außerhalb des Moduls implementiert ist, können Sie dem Basisnamen einen Unterstrich voranstellen, der ihn als privat für das Modul kennzeichnet (dh nicht importiert).

dh

class _Controller(object):
    path = '' # There are better ways to declare attributes - see other answers

class MyController(_Controller):
    path = '/Home'
Brendan
quelle
1
Ist es möglich, einen Fehler auszulösen, wenn die Unterklasse das Attribut nicht neu definiert? Es wäre einfach für Methoden, aber wie wäre es mit Attributen?
Mario F
Wäre es nicht besser, die Pfaddeklaration in der _ControllerKlasse wegzulassen? Duck Typing wird nicht wirksam, wenn bereits ein (ungültiger) Wert vorhanden ist. Andernfalls würde irgendwann, wenn das pathFeld definiert werden muss, kein Fehler auftreten, da bereits ein Wert vorhanden ist.
Deamon
@Mario - ja, Brendan Abels Antwort gibt einen guten Weg, dies zu tun
Brendan
1
class AbstractStuff:
    @property
    @abc.abstractmethod
    def some_property(self):
        pass

Ab 3.3 abc.abstractpropertyist ich veraltet.

Grisaitis
quelle