I want to have an abstract class which forces every derived class to set certain attributes in its __init__
method.
I've looked at several questions which did not fully solve my problem, specifically here or here. This looked promising but I couldn't manage to get it working.
I assume my desired outcome could look like the following pseudo code:
from abc import ABCMeta, abstractmethod
class Quadrature(object, metaclass=ABCMeta):
@someMagicKeyword #<==== This is what I want, but can't get working
xyz
@someMagicKeyword #<==== This is what I want, but can't get working
weights
@abstractmethod
def __init__(self, order):
pass
def someStupidFunctionDefinedHere(self, n):
return self.xyz+self.weights+n
class QuadratureWhichWorks(Quadrature):
# This shall work because we initialize xyz and weights in __init__
def __init__(self,order):
self.xyz = 123
self.weights = 456
class QuadratureWhichShallNotWork(Quadrature):
# Does not initialize self.weights
def __init__(self,order):
self.xyz = 123
Here are some of the things I have tried:
from abc import ABCMeta, abstractmethod
class Quadrature(object, metaclass=ABCMeta):
@property
@abstractmethod
def xyz(self):
pass
@property
@abstractmethod
def weights(self):
pass
@abstractmethod
def __init__(self, order):
pass
def someStupidFunctionDefinedHere(self, n):
return self.xyz+self.weights+n
class QuadratureWhichWorks(Quadrature):
# This shall work because we initialize xyz and weights in __init__
def __init__(self,order):
self.xyz = 123
self.weights = 456
Then I try to create an instance:
>>> from example1 import *
>>> Q = QuadratureWhichWorks(10)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: Can't instantiate abstract class QuadratureWhichWorks with abstract methods weights, xyz
>>>
Which tells me to implement the methods, but I thought I said that these are properties
?
My current work-around has the flaw, that the __init__
method can be overwritten in the derived classes but for now this at least ensures (to me) that I always know that the requested properties are set:
from abc import ABCMeta, abstractmethod
class Quadrature(object, metaclass=ABCMeta):
@abstractmethod
def computexyz(self,order):
pass
@abstractmethod
def computeweights(self,order):
pass
def __init__(self, order):
self.xyz = self.computexyz(order)
self.weights = self.computeweights(order)
def someStupidFunctionDefinedHere(self, n):
return self.xyz+self.weights+n
class QuadratureWhichWorks(Quadrature):
def computexyz(self,order):
return order*123
def computeweights(self,order):
return order*456
class HereComesTheProblem(Quadrature):
def __init__(self,order):
self.xyz = 123
# but nothing is done with weights
def computexyz(self,order):
return order*123
def computeweights(self,order): # will not be used
return order*456
But the problem is
>>> from example2 import *
>>> Q = HereComesTheProblem(10)
>>> Q.xyz
123
>>> Q.weights
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'HereComesTheProblem' object has no attribute 'weights'
How is this implemented correctly?
Abstract base classes cannot be instantiated. Instead, they are inherited and extended by the concrete subclasses. Subclasses derived from a specific abstract base class must implement the methods and properties provided in that abstract base class. Otherwise, an error is raised during the object instantiation.
An abstract class can be considered as a blueprint for other classes. It allows you to create a set of methods that must be created within any child classes built from the abstract class. A class which contains one or more abstract methods is called an abstract class.
When you create a new object of the class, Python automatically calls __init__() right away to initialize the new object. But in fact, the standard way to initialize an object's values when creating objects is to use __init__( ) method in class.
To define an abstract method we use the @abstractmethod decorator of the abc module. It tells Python that the declared method is abstract and should be overridden in the child classes. We just need to put this decorator over any function we want to make abstract and the abc module takes care of the rest.
It's worth noting that custom metaclasses are often frowned upon, but you can solve this problem with one.
Here is a good write up discussing how they work and when they're useful. The solution here is essentially to tack on a check for the attribute that you want after the __init__
is invoked.
from abc import ABCMeta, abstractmethod
# our version of ABCMeta with required attributes
class MyMeta(ABCMeta):
required_attributes = []
def __call__(self, *args, **kwargs):
obj = super(MyMeta, self).__call__(*args, **kwargs)
for attr_name in obj.required_attributes:
if not getattr(obj, attr_name):
raise ValueError('required attribute (%s) not set' % attr_name)
return obj
# similar to the above example, but inheriting MyMeta now
class Quadrature(object, metaclass=MyMeta):
required_attributes = ['xyz', 'weights']
@abstractmethod
def __init__(self, order):
pass
class QuadratureWhichWorks(Quadrature):
# This shall work because we initialize xyz and weights in __init__
def __init__(self,order):
self.xyz = 123
self.weights = 456
q = QuadratureWhichWorks('foo')
class QuadratureWhichShallNotWork(Quadrature):
def __init__(self, order):
self.xyz = 123
q2 = QuadratureWhichShallNotWork('bar')
Below is my original answer which explores the topic more in general.
I think some of this comes from confusing instance attributes with the objects wrapped by the property
decorator.
A small example without introducing abstract classes would be
>>> class Joker(object):
>>> # a class attribute
>>> setup = 'Wenn ist das Nunstück git und Slotermeyer?'
>>>
>>> # a read-only property
>>> @property
>>> def warning(self):
>>> return 'Joke Warfare is explicitly banned bythe Geneva Conventions'
>>>
>>> def __init__(self):
>>> self.punchline = 'Ja! Beiherhund das Oder die Flipperwaldt gersput!'
>>> j = Joker()
>>> # we can access the class attribute via class or instance
>>> Joker.setup == j.setup
>>> # we can get the property but cannot set it
>>> j.warning
'Joke Warfare is explicitly banned bythe Geneva Conventions'
>>> j.warning = 'Totally safe joke...'
AttributeError: cant set attribute
>>> # instance attribute set in __init__ is only accessible to that instance
>>> j.punchline != Joker.punchline
AttributeError: type object 'Joker' has no attribute 'punchline'
According to the Python docs, since 3.3 the abstractproperty
is redundant and actually reflects your attempted solution.
The issue with that solution is that your subclasses do not implement a concrete property, they just overwrite it with an instance attribute.
In order to continue using the abc
package, you could handle this by implementing those properties, i.e.
>>> from abc import ABCMeta, abstractmethod
>>> class Quadrature(object, metaclass=ABCMeta):
>>>
>>> @property
>>> @abstractmethod
>>> def xyz(self):
>>> pass
>>>
>>> @property
>>> @abstractmethod
>>> def weights(self):
>>> pass
>>>
>>> @abstractmethod
>>> def __init__(self, order):
>>> pass
>>>
>>> def someStupidFunctionDefinedHere(self, n):
>>> return self.xyz+self.weights+n
>>>
>>>
>>> class QuadratureWhichWorks(Quadrature):
>>> # This shall work because we initialize xyz and weights in __init__
>>> def __init__(self,order):
>>> self._xyz = 123
>>> self._weights = 456
>>>
>>> @property
>>> def xyz(self):
>>> return self._xyz
>>>
>>> @property
>>> def weights(self):
>>> return self._weights
>>>
>>> q = QuadratureWhichWorks('foo')
>>> q.xyz
123
>>> q.weights
456
I think this is a bit clunky though, but it really depends on how you intend to implment subclasses of Quadrature
.
My suggestion would be to not make xyz
or weights
abstract, but instead handle whether they were set at runtime, i.e. catch any AttributeError
s that may pop up when accessing the value.
In order to force a subclass to implement a property or method, you need to raise an error, if this method is not implemented:
from abc import ABCMeta, abstractmethod, abstractproperty
class Quadrature(object, metaclass=ABCMeta):
@abstractproperty
def xyz(self):
raise NotImplementedError
This is possible because of changes to python 3.7 (which I hope you are using - because this is cool!) as it adds type hinting
and the ability to add class annotations, which were added for dataclasses
. It is as close to your original desired syntax as I can think up. The superclass you will want would look something like this:
from abc import ABC, abstractmethod
from typing import List
class PropertyEnfocedABC(ABC):
def __init__(self):
annotations = self.__class__.__dict__.get('__annotations__', {})
for name, type_ in annotations.items():
if not hasattr(self, name):
raise AttributeError(f'required attribute {name} not present '
f'in {self.__class__}')
Now, to see it in action.
class Quadratic(PropertyEnfocedABC):
xyz: int
weights: List[int]
def __init__(self):
self.xyz = 2
self.weights = [4]
super().__init__()
or more accurately in your case, with a mix of abstract methods and attributes:
class Quadrature(PropertyEnforcedABC):
xyz: int
weights: int
@abstractmethod
def __init__(self, order):
pass
@abstractmethod
def some_stupid_function(self, n):
return self.xyz + self.weights + n
Now, any subclass of a subclass of a PropertyEnforcedABC
must set the attributes that are annotated in the class (if you do not provide a type to the annotation it will not be considered an annotation) and thus if the constructor of quadratic did not set xyz
or weights
, an attribute error would be raised. Note you must call the constructor at the end of init, but this should not be a real problem and you can solve this by wrapping your own metaclass around the above code if you really don't like it.
You could modify PropertyEnforcedABC
however you want (like enforcing the type of the properties) and more. You could even check for Optional
and ignore those.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With