开发者

Abstract attributes in Python [duplicate]

This question already has answers here: How to create abstract properties in python abstract classes? (7 answers) Closed 2 years ago.

What is the shortest / most elegant way to implement the following Scala code with an abstract attribute in Python?

abstract class Controller {

    val path: String

}

A subclass of Controller is enforced to define "path" by the S开发者_StackOverflowcala compiler. A subclass would look like this:

class MyController extends Controller {

    override val path = "/home"

}


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

Failure to declare a or b in the derived class B will raise a TypeError such as:

TypeError: Can't instantiate abstract class B with abstract methods a

Python 2.7

There is an @abstractproperty decorator for this:

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


Python has a built-in exception for this, though you won't encounter the exception until runtime.

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


class SubClass(Base):
    path = 'blah'


Since this question was originally asked, python has changed how abstract classes are implemented. I have used a slightly different approach using the abc.ABC formalism in python 3.6. Here I define the constant as a property which must be defined in each subclass.

from abc import ABC, abstractmethod


class Base(ABC):

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

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


class Derived(Base):
    CONSTANT = 42

This forces the derived class to define the constant, or else a TypeError exception will be raised when you try to instantiate the subclass. When you want to use the constant for any functionality implemented in the abstract class, you must access the subclass constant by type(self).CONSTANT instead of just CONSTANT, since the value is undefined in the base class.

There are other ways to implement this, but I like this syntax as it seems to me the most plain and obvious for the reader.

The previous answers all touched useful points, but I feel the accepted answer does not directly answer the question because

  • The question asks for implementation in an abstract class, but the accepted answer does not follow the abstract formalism.
  • The question asks that implementation is enforced. I would argue that enforcement is stricter in this answer because it causes a runtime error when the subclass is instantiated if CONSTANT is not defined. The accepted answer allows the object to be instantiated and only throws an error when CONSTANT is accessed, making the enforcement less strict.

This is not to fault the original answers. Major changes to the abstract class syntax have occurred since they were posted, which in this case allow a neater and more functional implementation.


In Python 3.6+, you can annotate an attribute of an abstract class (or any variable) without providing a value for that attribute.

from abc import ABC

class Controller(ABC):
    path: str

class MyController(Controller):
    def __init__(self, path: str):
        self.path = path

This makes for very clean code where it is obvious that the attribute is abstract. Code that tries to access the attribute when if has not been overwritten will raise an AttributeError.


You could create an attribute in the abc.ABC abstract base class with a value such as NotImplemented so that if the attribute is not overriden and then used, a clear error that expresses intent is shown at run time.

The following code uses a PEP 484 type hint to help PyCharm correctly statically analyze the type of the path attribute as well.

from abc import ABC

class Controller(ABC):
    path: str = NotImplemented

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


For Python 3.3+ there's an elegant solution

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
        """

Despite some great answers have already been given, I thought this answer would nevertheless add some value. This approach has two advantages:

  1. ... in an abstract method's body is more preferable than pass. Unlike pass, ... implies no operations, where pass only means the absence of an actual implementation

  2. ... is more recommended than throwing NotImplementedError(...). This automatically prompts an extremely verbose error if the implementation of an abstract field is missing in a subclass. In contrast, NotImplementedError itself doesn't tell why the implementation is missing. Moreover, it requires manual labor to actually raise it.


As of Python 3.6 you can use __init_subclass__ to check for the class variables of the child class upon initialisation:

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

This raises an Error on initialisation of the child class, if the missing class variable is not defined, so you don't have to wait until the missing class variable would be accessed.


I've modified just a bit @James answer, so that all those decorators do not take so much place. If you had multiple such abstract properties to define, this is handy:

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


Python3.6 implementation might looks like this:

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>


Your base class could implement a __new__ method that check for class attribute:

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

This way the error raise at instantiation


Bastien Léonard's answer mentions the abstract base class module and Brendan Abel's answer deals with non-implemented attributes raising errors. To ensure that the class is not implemented outside of the module, you could prefix the base name with an underscore which denotes it as private to the module (i.e. it is not imported).

i.e.

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

class MyController(_Controller):
    path = '/Home'


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

As of 3.3 abc.abstractproperty is deprecated, I think.


Have a look at the abc (Abtract Base Class) module: http://docs.python.org/library/abc.html

However, in my opinion the simplest and most common solution is to raise an exception when an instance of the base class is created, or when its property is accessed.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜