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?
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']
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)
}
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'}
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?
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']
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)
}
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'}