Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Inheriting setter, overwriting getter in python abstract class

Say you have an attribute in a base class with a single setter method that will be used in all subclasses, but with different getter methods in all subclasses. Ideally you only want to write the code for the setter method once in the base class. Additionally, you want to make the base class abstract because it doesn't have a complete getter method. Is there any way to do this?

Naively, I think it should go something like this:

class _mystring(object):

    __metaclass__ = abc.ABCMeta

    @abc.abstractproperty
    def str(self):
        pass

    @str.setter
    def str(self,value):
        self._str = value

class uppercase(_mystring):

    @_mystring.str.getter
    def str(self):
        return self._str.upper()

But upper = uppercase() fails with Can't instantiate abstract class uppercase with abstract methods str.

If I change the @abc.abstractproperty in class _mystring to @property, then upper = uppercase() works, but so does upper = _mystring(), which I don't want.

What am I missing?

Thanks!

like image 333
Seanslice Avatar asked Oct 20 '22 21:10

Seanslice


1 Answers

I managed to make a small wrapper around abstractproperty that will do it:

class partialAP(abc.abstractproperty):
    def getter(self, func):
        if getattr(func, '__isabstractmethod__', False) or getattr(self.fset, '__isabstractmethod__', False):
            p = partialAP(func, self.fset)
        else:
            p = property(func, self.fset)
        return p

    def setter(self, func):
        if getattr(self.fget, '__isabstractmethod__', False) or getattr(func, '__isabstractmethod__', False):
            p = partialAP(self.fget, func)
        else:
            p = property(self.fset, func)
        return p

Then your code works with just a slight modification:

class _mystring(object):

    __metaclass__ = abc.ABCMeta

    @partialAP
    @abc.abstractmethod
    def str(self):
        pass

    @str.setter
    def str(self,value):
        self._str = value

class uppercase(_mystring):

    @_mystring.str.getter
    def str(self):
        return self._str.upper()

And then the behavior is as desired:

>>> _mystring()
Traceback (most recent call last):
  File "<pyshell#3>", line 1, in <module>
    _mystring()
TypeError: Can't instantiate abstract class _mystring with abstract methods str
>>> u = uppercase()
>>> u.str = 'a'
>>> u.str
'A'

My partialAP wrapper must be used in conjuction with abc.abstractmethod. What you do is you use abstractmethod to decorate the "piece" of the property (getter or setter) that you want to be abstract, and use partialAP as a second decorator. partialAP defines getter/setter functions that replace the property with a normal one only if both getter and setter are non-abstract.

Obviously you'd have to extend this a bit to make it worth with property deleters too. There could also be corner cases that won't be handled right if you have a more complex inheritance hierarchy.

Old solution for posterity:

class _mystring(object):

    __metaclass__ = abc.ABCMeta

    @abc.abstractmethod
    def _strGet(self):
        pass

    def _strSet(self,value):
        self._str = value

    str = property(_strGet, _strSet)

class uppercase(_mystring):

    def _strGet(self):
        return self._str.upper()
    str = _mystring.str.getter(_strGet)

Then it works:

>>> _mystring()
Traceback (most recent call last):
  File "<pyshell#39>", line 1, in <module>
    _mystring()
TypeError: Can't instantiate abstract class _mystring with abstract methods _strGet
>>> u = uppercase()
>>> u.str = 'a'
>>> u.str
'A'

The trick is that you have to not use the convenient decorators. The abstractproperty decorator marks the entire property as abstract. What you have to do is create two methods, an abstract one (for the getter) and a regular one (for the setter), then create a regular property that combines them. Then when you extend the class, you must override the abstract getter and explicitly "mix" it with the base class property (using _mystring.str.getter).

The reason you can't use @_mystring.str.getter is that the decorator forces both the getter and the setter to have the same name as the property itself. If you want to mark just one of the two as abstract, they have to have different names or you can't get at them separately.

Note that this solution requires subclasses to give their getters the right name (_strGet in this case), in order to satisfy the ABC that they have overriden the abstract method.

like image 64
BrenBarn Avatar answered Oct 24 '22 00:10

BrenBarn