Python abstract setters and getters

1.6k Views Asked by At

I want to write abstract class that will force inheriting classes to implement all methods AND properties in my abstract class.

Additionally I want to use of setters and getters for my abstract property to make my code uncluttered and looking nicely

However, current implementation:

import abc


class Component(metaclass=abc.ABCMeta):

    @property
    @abc.abstractmethod
    def status(self):
        pass

    @property
    @status.setter
    @abc.abstractmethod
    def status(self, value):
        pass

does enforce inheriting class to implement getter for my abstract property getter, but does not enforce creating a property setter (what is exactly what I want)

How can I achieve this behavior without loosing all benefits from application of further mentioned method (aka writing new methods and executing them in my abstract class setter) ?

2

There are 2 best solutions below

0
On

The problem is that neither the getter nor the setter is a method of your abstract class; they are attributes of the property, which is a (non-callable) class attribute. Consider this equivalent definition:

def status_getter(self):
    pass

def status_setter(self, value):
    pass

class Component(metaclass=abc.ABCMeta):
    # status = property(...)
    # status.__isabstractmethod__ = True
    status = abstractmethod(property(status_getter, status_setter))

Inheriting a property is quite different from inheriting a method. You are basically replacing the property, because your class itself does not have a reference to either the getter or the setter. Despite the name, abstractmethod does not actually make the property a method; it really does nothing more than add an attribute to whatever it is applied to and return the original value.

So, to ensure that a subclass provides a read/write property, what are you to do? Skip the decorator syntax, define the getter and setter as explicit abstract methods, then define the property explicitly in terms of those private methods.

class Component(metaclass=abc.ABCMeta):
    @abstractmethod
    def _get_status(self):
        pass

    @abstractmethod
    def _set_status(self, v):
        pass

    status = property(lambda self: self._get_status(), lambda self, v: self._set_status(self, v))

Or, you can make use of __init_subclass__ (which postdates abc; its purpose is to allow class initialization that is otherwise only possible via a metaclass).

class Component:
    def __init_subclass(cls, **kwargs):
        super().__init_subclass__(**kwargs)

        try:
            p = cls.status
        except AttributeError:
            raise ValueError("Class does not define 'status' attribute")

        if not isinstance(p, property):
            raise ValueError("'status' is not a property")

        if p.fget is None:
            raise ValueError("'status' has no getter")

        if p.fset is None:
            raise ValueError("'status' has no setter")

This is actually an improvement over abc, in my opinion. If a subclass fails to define a read/write status property, an exception will be raised when the class is defined, not just when you attempt to instantiate the class.

0
On
from abc import ABCMeta, abstractmethod

class Base(object):
    __metaclass__ = ABCMeta

    def __init__(self, val):
        self._foo = val

    @abstractmethod
    def _doStuff(self, signals):
        print ('Base does stuff')

    @abstractmethod
    def _get_foo(self):
        return self._foo

    @abstractmethod
    def _set_foo(self, val):
        self._foo = val + 'r'

    foo = property(_get_foo, _set_foo)

class floor_1(Base):
    __metaclass__ = ABCMeta

    def __init__(self, val):
        self._foo = val
        super(floor_1, self).__init__(val)

    def _doStuff(self, signals):
        print ('floor_1 does stuff')

    def _get_foo(self):
        return self._foo

    def _set_foo(self, val):
        #self._foo = val + 'r'
        super()._set_foo(val + 'r')

    foo = property(_get_foo, _set_foo)

class floor_2(floor_1):

    @property
    def foo(self):
        return self._foo

    @foo.setter
    def foo(self, val):
        self._foo = val + 'r'
        #super()._set_foo(val + 'r')

b1 = floor_1('bar')
# b1 = floor_2('bar')
print(b1.foo)
b1.foo = 'bar'
print(b1.foo)