Abstracte attributen in Python

Wat is de kortste/meest elegante manier om de volgende Scala-code met een abstract attribuut in Python te implementeren?

abstract class Controller {
    val path: String
}

Een subklasse van Controllerwordt afgedwongen om “pad” te definiëren door de Scala-compiler. Een subklasse ziet er als volgt uit:

class MyController extends Controller {
    override val path = "/home"
}

Antwoord 1, autoriteit 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

Het niet declareren van aof Bin de afgeleide klasse Bzal een TypeErrorveroorzaken, zoals:

TypeError: kan abstracte klasse Bniet instantiëren met abstracte methoden a

Python 2.7

Er is een @abstractproperty-decorateur hiervoor:

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

Antwoord 2, autoriteit 84%

Python heeft hiervoor een ingebouwde uitzondering, maar u zult de uitzondering pas tijdens runtime tegenkomen.

class Base(object):
    @property
    def path(self):
        raise NotImplementedError
class SubClass(Base):
    path = 'blah'

Antwoord 3, autoriteit 49%

Sinds deze vraag oorspronkelijk werd gesteld, heeft Python de manier veranderd waarop abstracte klassen worden geïmplementeerd. Ik heb een iets andere benadering gebruikt met behulp van het abc.ABC-formalisme in python 3.6. Hier definieer ik de constante als een eigenschap die in elke subklasse moet worden gedefinieerd.

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

Dit dwingt de afgeleide klasse om de constante te definiëren, anders wordt er een TypeError-uitzondering gegenereerd wanneer je probeert de subklasse te instantiëren. Als u de constante wilt gebruiken voor functionaliteit die in de abstracte klasse is geïmplementeerd, moet u de constante van de subklasse benaderen met type(self).CONSTANTin plaats van alleen CONSTANT, aangezien de waarde is niet gedefinieerd in de basisklasse.

Er zijn andere manieren om dit te implementeren, maar ik hou van deze syntaxis omdat het mij de meest duidelijke en voor de hand liggende lijkt voor de lezer.

De vorige antwoorden raakten allemaal nuttige punten, maar ik heb het gevoel dat het geaccepteerde antwoord de vraag niet direct beantwoordt, omdat

  • De vraag vraagt ​​om implementatie in een abstracte klasse, maar het geaccepteerde antwoord volgt niet het abstracte formalisme.
  • De vraag is of de implementatie wordt afgedwongen. Ik zou willen beweren dat de handhaving in dit antwoord strenger is omdat het een runtime-fout veroorzaakt wanneer de subklasse wordt geïnstantieerd als CONSTANTniet is gedefinieerd. Met het geaccepteerde antwoord kan het object worden geïnstantieerd en wordt alleen een fout gegenereerd wanneer CONSTANTwordt geopend, waardoor de handhaving minder strikt wordt.

Dit is niet in strijd met de oorspronkelijke antwoorden. Er zijn grote wijzigingen in de syntaxis van de abstracte klassen opgetreden sinds ze zijn gepost, wat in dit geval een nettere en functionelere implementatie mogelijk maakt.


Antwoord 4, autoriteit 31%

In Python 3.6+kun je een attribuut van een abstract annoteren class (of een andere variabele) zonder een waarde voor dat attribuut op te geven.

from abc import ABC
class Controller(ABC):
    path: str
class MyController(Controller):
    path = "/home"

Dit zorgt voor een zeer schone code waarbij het duidelijk is dat het attribuut abstract is. Code die probeert toegang te krijgen tot het attribuut wanneer if niet is overschreven, zal een AttributeErroropleveren.


Antwoord 5, autoriteit 16%

U kunt een kenmerk maken in de abc.ABCabstracte basisklasse met een waarde zoals NotImplementedzodat als het kenmerk niet wordt overschreven en vervolgens wordt gebruikt, er tijdens runtime een fout wordt weergegeven.

De volgende code gebruikt een PEP 484hint om PyCharm te helpen ook het type van het path-attribuut correct statisch analyseren.

from abc import ABC
class Controller(ABC):
    path: str = NotImplemented
class MyController(Controller):
    path = "/home"

Antwoord 6, autoriteit 14%

Voor Python 3.3+ is er een elegante oplossing

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 for clarity
class AnotherBaseController(ABC):
    @property
    @abstractmethod
    def path(self) -> str:
    """
    :return: the url path of this controller
    """

Ondanks dat er al enkele goede antwoorden zijn gegeven, dacht ik dat dit antwoord toch enige waarde zou toevoegen. Deze aanpak heeft twee voordelen:

  1. ...in de body van een abstracte methode heeft meer de voorkeur dan pass. In tegenstelling tot pass, impliceert ...geen bewerkingen, waarbij passalleen de afwezigheid van een daadwerkelijke implementatie betekent

  2. ...is meer aanbevolen dan het gooien van NotImplementedError(...). Dit veroorzaakt automatisch een extreem uitgebreide fout als de implementatie van een abstract veld in een subklasse ontbreekt. Daarentegen vertelt NotImplementedErrorzelf niet waarom de implementatie ontbreekt. Bovendien vereist het handenarbeid om het daadwerkelijk op te tillen.


Antwoord 7, autoriteit 10%

Vanaf Python 3.6 kun je __init_subclass__gebruiken om te controleren op de klassenvariabelen van de onderliggende klasse bij initialisatie:

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

Dit geeft een foutmelding bij het initialiseren van de onderliggende klasse, als de ontbrekende klassevariabele niet is gedefinieerd, zodat u niet hoeft te wachten tot de ontbrekende klassevariabele wordt geopend.


Antwoord 8, autoriteit 4%

Ik heb een klein @James-antwoord aangepast, zodat al die decorateurs niet zoveel plaats innemen. Als u meerdere van dergelijke abstracte eigenschappen moest definiëren, is dit handig:

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

Antwoord 9, autoriteit 4%

Python3.6-implementatie kan er als volgt uitzien:

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>

Antwoord 10, autoriteit 4%

Uw basisklasse zou een __new__-methode kunnen implementeren die controleert op class-attribuut:

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

Op deze manier neemt de fout toe bij het starten


Antwoord 11

Het antwoord van Bastien Léonard vermeldt de abstracte basisklasse-module en het antwoord van Brendan Abel gaat over niet-geïmplementeerde attributen die fouten veroorzaken. Om ervoor te zorgen dat de klasse niet buiten de module wordt geïmplementeerd, kunt u de basisnaam vooraf laten gaan door een onderstrepingsteken dat aangeeft dat het privé is voor de module (d.w.z. het wordt niet geïmporteerd).

d.w.z.

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

Antwoord 12

class AbstractStuff:
    @property
    @abc.abstractmethod
    def some_property(self):
        pass

Vanaf 3.3 is abc.abstractpropertyverouderd, denk ik.


Antwoord 13

Bekijk de abc (Abtract Base Class) module: http://docs. python.org/library/abc.html

Naar mijn mening is de eenvoudigste en meest gebruikelijke oplossing echter om een ​​uitzondering te maken wanneer een instantie van de basisklasse wordt gemaakt of wanneer de eigenschap wordt geopend.

Other episodes