pythonoopabstract-class

Python abstract class shall force derived classes to initialize variable in __init__


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?


Solution

  • Solution with a custom metaclass

    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.

    Original Answer

    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 AttributeErrors that may pop up when accessing the value.