Is there a way to avoid boilerplate property getters in Python subclasses using the @property decorator?

Question:

I am trying to create subclasses of some superclass that has properties (delineated by the property decorator) and a properties() method that returns the properties and their values as a dict. I want the subclasses to be able to make use of the inherited properties() method with minimal boilerplate code needing to be copied and pasted into each subclass definition. I’m using Python 3.8 if it makes a difference here.

The emphasis here is on the boilerplate code that I want to refactor.

I begin with a simple class, BaseClass, defined as follows:

class BaseClass(object):
    def __init__(self, A, B):
        self._A = A
        self._B = B
    
    @property
    def A(self):
        return self._A
    
    @A.setter
    def A(self, new_value):
        # ... Validate input ...
        self._A = new_value

    @property
    def B(self):
        return self._B

    @B.setter
    def B(self, new_value):
        # ... Validate input ...
        self._B = new_value

    def properties(self):
        class_items = self.__class__.__dict__.items()
        return dict((k, getattr(self, k)) for k, v in class_items if isinstance(v, property))

I should note that I defined the properties() method at the end after reading this other stackoverflow answer.

Example usage of BaseClass as defined above:

>>> base_instance = BaseClass('foo','bar')
>>> base_instance.properties()
{'A': 'foo', 'B': 'bar'}

From there, I want to create some subclasses that inherit from BaseClass and have the properties() method work the same way. Consider the following:

class SubClass(BaseClass):
    def __init__(self, A, B):
        super().__init__(A, B)

But this doesn’t behave the way I expected:

>>> sub_instance = SubClass('spam',10)
>>> sub_instance.A  # works as expected
'spam'
>>> sub_instance.B  # works as expected
10
>>> sub_instance.properties()  # expected {'A': 'spam', 'B': 10}
{}

I know that the following alternative subclass definition produces the behavior I expected:

class SubClass(BaseClass):
    def __init__(self, A, B):
        super().__init__(A, B)

    @BaseClass.A.getter    #
    def A(self):           #  This is all boilerplate
        return self._A     #  that I need to copy and
                           #  paste in every subclass
    @BaseClass.B.getter    #  of BaseClass I define...
    def B(self):           #
        return self._B     #
>>> sub_instance = SubClass('spam',10)
>>> sub_instance.properties()
{'A': 'spam', 'B': 10}

Is there a cleaner way to define properties (with the property decorator) inside the superclass BaseClass so that the properties() method "just works", without the need for the boilerplate lines?

Asked By: BentPen

||

Answers:

You can try to use inspect.getmro to get all base classes:

from inspect import getmro


class BaseClass(object):
    def __init__(self, A, B):
        self._A = A
        self._B = B

    @property
    def A(self):
        return self._A

    @A.setter
    def A(self, new_value):
        self._A = new_value

    @property
    def B(self):
        return self._B

    @B.setter
    def B(self, new_value):
        self._B = new_value

    def properties(self):
        out = []
        for klass in getmro(self.__class__):
            class_items = klass.__dict__.items()
            out.extend(
                dict(
                    (k, getattr(self, k))
                    for k, v in class_items
                    if isinstance(v, property)
                )
            )
        return out


class SubClass(BaseClass):
    def __init__(self, A, B):
        super().__init__(A, B)

    @property
    def C(self):
        return 42


sub_instance = SubClass("spam", 10)
print(sub_instance.properties())

Prints:

['C', 'A', 'B']
Answered By: Andrej Kesely

You should fix the way you define properties to walk the whole MRO:

def properties(self):
    klasses = reversed(type(self).mro())
    return {
        k: getattr(self, k) 
        for klass in klasses 
        for k, v in klass.items() 
        if isinstance(v, property)
    }
Answered By: juanpa.arrivillaga

To somewhat reduce the boiler plate code of properties, you could create function automatically implement storage to and retrieval from the specified internal variable.

def autoProperty(varName):
    g = dict()
    exec(f"def getProp(self): return self.{varName}",g)
    exec(f"def setProp(self,value): self.{varName} = value",g)
    return property(g["getProp"],g["setProp"])  

        

Using this function you can define read/write properties with a single line and you can use it as a decorator (with the .setter suffix) when you need special processing on assignment:

class BaseClass:

    A = autoProperty("_A")
    
    @autoProperty("_B").setter
    def B(self,value):
        print("changing B to",value)
        self._B = value
        
    def __init__(self,A,B):
        self._A = A
        self._B = B

    def properties(self):
        return { name:getattr(self,name) for name in dir(self.__class__)
                 if isinstance(getattr(self.__class__,name),property) }

This will produce the list of properties in your properties() method.

base_instance = BaseClass('foo','bar')
print(base_instance.properties())

{'A': 'foo', 'B': 'bar'}

Note that I used dir() instead of __class__.__dict__ because the subclasses will not have the superclass’s property names in their __dict__

When defining a sub class, you can add more properties using the same approach:

class SubClass(BaseClass):

    C = autoProperty("_C")
    
    def __init__(self,A,B):
        super().__init__(A,B)
        self.C = A + "-" + B

The properties() method now accesses the cumulative properties of both the sub and super class:

sub_instance = SubClass('spam','10')
print(sub_instance.properties())

{'A': 'spam', 'B': '10', 'C': 'spam-10'}
Answered By: Alain T.
Categories: questions Tags: , , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.