Subclassing: Is it possible to override a property with a conventional attribute?
This will be a long winded answer that might only serve to be complimentary... but your question took me for a ride down the rabbit hole so I'd like to share my findings (and pain) as well.
You might ultimately find this answer not helpful to your actual problem. In fact, my conclusion is that - I wouldn't do this at all. Having said that, the background to this conclusion might entertain you a bit, since you're looking for more details.
Addressing some misconception
The first answer, while correct in most cases, is not always the case. For instance, consider this class:
class Foo:
def __init__(self):
self.name = 'Foo!'
@property
def inst_prop():
return f'Retrieving {self.name}'
self.inst_prop = inst_prop
inst_prop
, while being a property
, is irrevocably an instance attribute:
>>> Foo.inst_prop
Traceback (most recent call last):
File "<pyshell#60>", line 1, in <module>
Foo.inst_prop
AttributeError: type object 'Foo' has no attribute 'inst_prop'
>>> Foo().inst_prop
<property object at 0x032B93F0>
>>> Foo().inst_prop.fget()
'Retrieving Foo!'
It all depends where your property
is defined in the first place. If your @property
is defined within the class "scope" (or really, the namespace
), it becomes a class attribute. In my example, the class itself isn't aware of any inst_prop
until instantiated. Of course, it's not very useful as a property at all here.
But first, let's address your comment on inheritance resolution...
So how exactly does inheritance factor into this issue? This following article dives a little bit into the topic, and the Method Resolution Order is somewhat related, though it discusses mostly the Breadth of inheritance instead of Depth.
Combined with our finding, given these below setup:
@property
def some_prop(self):
return "Family property"
class Grandparent:
culture = some_prop
world_view = some_prop
class Parent(Grandparent):
world_view = "Parent's new world_view"
class Child(Parent):
def __init__(self):
try:
self.world_view = "Child's new world_view"
self.culture = "Child's new culture"
except AttributeError as exc:
print(exc)
self.__dict__['culture'] = "Child's desired new culture"
Imagine what happens when these lines are executed:
print("Instantiating Child class...")
c = Child()
print(f'c.__dict__ is: {c.__dict__}')
print(f'Child.__dict__ is: {Child.__dict__}')
print(f'c.world_view is: {c.world_view}')
print(f'Child.world_view is: {Child.world_view}')
print(f'c.culture is: {c.culture}')
print(f'Child.culture is: {Child.culture}')
The result is thus:
Instantiating Child class... can't set attribute c.__dict__ is: {'world_view': "Child's new world_view", 'culture': "Child's desired new culture"} Child.__dict__ is: {'__module__': '__main__', '__init__': <function Child.__init__ at 0x0068ECD8>, '__doc__': None} c.world_view is: Child's new world_view Child.world_view is: Parent's new world_view c.culture is: Family property Child.culture is: <property object at 0x00694C00>
Notice how:
self.world_view
was able to be applied, whileself.culture
failedculture
does not exist inChild.__dict__
(themappingproxy
of the class, not to be confused with the instance__dict__
)- Even though
culture
exists inc.__dict__
, it is not referenced.
You might be able to guess why - world_view
was overwritten by Parent
class as a non-property, so Child
was able to overwrite it as well. Meanwhile, since culture
is inherited, it only exists within the mappingproxy
of Grandparent
:
Grandparent.__dict__ is: {
'__module__': '__main__',
'culture': <property object at 0x00694C00>,
'world_view': <property object at 0x00694C00>,
...
}
In fact if you try to remove Parent.culture
:
>>> del Parent.culture Traceback (most recent call last): File "<pyshell#67>", line 1, in <module> del Parent.culture AttributeError: culture
You will notice it doesn't even exist for Parent
. Because the object is directly referring back to Grandparent.culture
.
So, what about the Resolution Order?
So we are interested to observe the actual Resolution Order, let's try removing Parent.world_view
instead:
del Parent.world_view
print(f'c.world_view is: {c.world_view}')
print(f'Child.world_view is: {Child.world_view}')
Wonder what the result is?
c.world_view is: Family property Child.world_view is: <property object at 0x00694C00>
It reverted back to Grandparent's world_view
property
, even though we had successfully manage to assign the self.world_view
before! But what if we forcefully change world_view
at the class level, like the other answer? What if we delete it? What if we assign the current class attribute to be a property?
Child.world_view = "Child's independent world_view"
print(f'c.world_view is: {c.world_view}')
print(f'Child.world_view is: {Child.world_view}')
del c.world_view
print(f'c.world_view is: {c.world_view}')
print(f'Child.world_view is: {Child.world_view}')
Child.world_view = property(lambda self: "Child's own property")
print(f'c.world_view is: {c.world_view}')
print(f'Child.world_view is: {Child.world_view}')
The result is:
# Creating Child's own world view c.world_view is: Child's new world_view Child.world_view is: Child's independent world_view # Deleting Child instance's world view c.world_view is: Child's independent world_view Child.world_view is: Child's independent world_view # Changing Child's world view to the property c.world_view is: Child's own property Child.world_view is: <property object at 0x020071B0>
This is interesting because c.world_view
is restored to its instance attribute, while Child.world_view
is the one we assigned. After removing the instance attribute, it reverts to the class attribute. And after reassigning the Child.world_view
to the property, we instantly lose access to the instance attribute.
Therefore, we can surmise the following resolution order:
- If a class attribute exists and it is a
property
, retrieve its value viagetter
orfget
(more on this later). Current class first to Base class last. - Otherwise, if an instance attribute exist, retrieve the instance attribute value.
- Else, retrieve the non-
property
class attribute. Current class first to Base class last.
In that case, let's remove the root property
:
del Grandparent.culture
print(f'c.culture is: {c.culture}')
print(f'Child.culture is: {Child.culture}')
Which gives:
c.culture is: Child's desired new culture Traceback (most recent call last): File "<pyshell#74>", line 1, in <module> print(f'Child.culture is: {Child.culture}') AttributeError: type object 'Child' has no attribute 'culture'
Ta-dah! Child
now has their own culture
based on the forceful insertion into c.__dict__
. Child.culture
doesn't exist, of course, since it was never defined in Parent
or Child
class attribute, and Grandparent
's was removed.
Is this the root cause of my problem?
Actually, no. The error you're getting, which we're still observing when assigning self.culture
, is totally different. But the inheritance order sets the backdrop to the answer - which is the property
itself.
Besides the previously mentioned getter
method, property
also have a few neat tricks up its sleeves. The most relevant in this case is the setter
, or fset
method, which is triggered by self.culture = ...
line. Since your property
didn't implement any setter
or fget
function, python doesn't know what to do, and throws an AttributeError
instead (i.e. can't set attribute
).
If however you implemented a setter
method:
@property
def some_prop(self):
return "Family property"
@some_prop.setter
def some_prop(self, val):
print(f"property setter is called!")
# do something else...
When instantiating the Child
class you will get:
Instantiating Child class... property setter is called!
Instead of receiving an AttributeError
, you are now actually calling the some_prop.setter
method. Which gives you more control over your object... with our previous findings, we know that we need to have a class attribute overwritten before it reaches the property. This could be implemented within the base class as a trigger. Here's a fresh example:
class Grandparent:
@property
def culture(self):
return "Family property"
# add a setter method
@culture.setter
def culture(self, val):
print('Fine, have your own culture')
# overwrite the child class attribute
type(self).culture = None
self.culture = val
class Parent(Grandparent):
pass
class Child(Parent):
def __init__(self):
self.culture = "I'm a millennial!"
c = Child()
print(c.culture)
Which results in:
Fine, have your own culture I'm a millennial!
TA-DAH! You can now overwrite your own instance attribute over an inherited property!
So, problem solved?
... Not really. The problem with this approach is, now you can't have a proper setter
method. There are cases where you do want to set values on your property
. But now whenever you set self.culture = ...
it will always overwrite whatever function you defined in the getter
(which in this instance, really is just the @property
wrapped portion. You can add in more nuanced measures, but one way or another it'll always involve more than just self.culture = ...
. e.g.:
class Grandparent:
# ...
@culture.setter
def culture(self, val):
if isinstance(val, tuple):
if val[1]:
print('Fine, have your own culture')
type(self).culture = None
self.culture = val[0]
else:
raise AttributeError("Oh no you don't")
# ...
class Child(Parent):
def __init__(self):
try:
# Usual setter
self.culture = "I'm a Gen X!"
except AttributeError:
# Trigger the overwrite condition
self.culture = "I'm a Boomer!", True
It's waaaaay more complicated than the other answer, size = None
at the class level.
You could also consider writing your own descriptor instead to handle the __get__
and __set__
, or additional methods. But at the end of the day, when self.culture
is referenced, the __get__
will always be triggered first, and when self.culture = ...
is referenced, __set__
will always be triggered first. There's no getting around it as far as I've tried.
The crux of the issue, IMO
The problem I see here is - you can't have your cake and eat it too. property
is meant like a descriptor with convenient access from methods like getattr
or setattr
. If you also want these methods to achieve a different purpose, you're just asking for trouble. I would perhaps rethink the approach:
- Do I really need a
property
for this? - Could a method serve me any differently?
- If I need a
property
, is there any reason I would need to overwrite it? - Does the subclass really belong in the same family if these
property
don't apply? - If I do need to overwrite any/all
property
s, would a separate method serve me better than simply reassigning, since reassigning can accidentally void theproperty
s?
For point 5, my approach would be have an overwrite_prop()
method in the base class that overwrite the current class attribute so that the property
will no longer be triggered:
class Grandparent:
# ...
def overwrite_props(self):
# reassign class attributes
type(self).size = None
type(self).len = None
# other properties, if necessary
# ...
# Usage
class Child(Parent):
def __init__(self):
self.overwrite_props()
self.size = 5
self.len = 10
As you can see, while still a bit contrived, it is at least more explicit than a cryptic size = None
. That said, ultimately, I wouldn't overwrite the property at all, and would reconsider my design from the root.
If you have made it this far - thank you for walking this journey with me. It was a fun little exercise.
A @property
is defined at the class level. The documentation goes into exhaustive detail on how it works, but suffice it to say that setting or getting the property resolve into calling a particular method. However, the property
object that manages this process is defined with the class's own definition. That is, it's defined as a class variable but behaves like an instance variable.
One consequence of this is that you can reassign it freely at the class level:
print(Math_Set_Base.size)
# <property object at 0x10776d6d0>
Math_Set_Base.size = 4
print(Math_Set_Base.size)
# 4
And just like any other class-level name (e.g. methods), you can override it in a subclass by just explicitly defining it differently:
class Square_Integers_Below(Math_Set_Base):
# explicitly define size at the class level to be literally anything other than a @property
size = None
def __init__(self,cap):
self.size = int(math.sqrt(cap))
print(Square_Integers_Below(4).size) # 2
print(Square_Integers_Below.size) # None
When we create an actual instance, the instance variable simply shadows the class variable of the same name. The property
object normally uses some shenanigans to manipulate this process (i.e. applying getters and setters) but when the class-level name isn't defined as a property, nothing special happens, and so it acts as you'd expect of any other variable.
A property is a data descriptor which takes precedence over an instance attribute with the same name. You could define a non-data descriptor with a unique __get__()
method: an instance attribute takes precedence over the non-data descriptor with the same name, see the docs. The problem here is that the non_data_property
defined below is for computation purpose only (you can't define a setter or a deleter) but it seems to be the case in your example.
import math
class non_data_property:
def __init__(self, fget):
self.__doc__ = fget.__doc__
self.fget = fget
def __get__(self, obj, cls):
if obj is None:
return self
return self.fget(obj)
class Math_Set_Base:
@non_data_property
def size(self, *elements):
return len(self.elements)
class Concrete_Math_Set(Math_Set_Base):
def __init__(self, *elements):
self.elements = elements
class Square_Integers_Below(Math_Set_Base):
def __init__(self, cap):
self.size = int(math.sqrt(cap))
print(Concrete_Math_Set(1, 2, 3).size) # 3
print(Square_Integers_Below(1).size) # 1
print(Square_Integers_Below(4).size) # 2
print(Square_Integers_Below(9).size) # 3
However this assumes that you have access to the base class in order to make this changes.
You don't need assignment (to size
) at all. size
is a property in the base class, so you can override that property in the child class:
class Math_Set_Base:
@property
def size(self):
return len(self.elements)
# size = property(lambda self: self.elements)
class Square_Integers_Below(Math_Set_Base):
def __init__(self, cap):
self._cap = cap
@property
def size(self):
return int(math.sqrt(self._cap))
# size = property(lambda self: int(math.sqrt(self._cap)))
You can (micro)optimize this by precomputing the square root:
class Square_Integers_Below(Math_Set_Base):
def __init__(self, cap):
self._size = int(math.sqrt(self._cap))
@property
def size(self):
return self._size