Abstract attributes in Python [duplicate]
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 classB
with abstract methodsa
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 whenCONSTANT
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:
...
in an abstract method's body is more preferable thanpass
. Unlikepass
,...
implies no operations, wherepass
only means the absence of an actual implementation...
is more recommended than throwingNotImplementedError(...)
. 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.
精彩评论