Let's assume we want to create a family of classes which are different implementations or specializations of an overarching concept. Let's assume there is a plausible default implementation for some derived properties. We'd want to put this into a base class
class Math_Set_Base: @property def size(self): return len(self.elements)
So a subclass will automatically be able to count its elements in this rather silly example
class Concrete_Math_Set(Math_Set_Base): def __init__(self,*elements): self.elements = elements Concrete_Math_Set(1,2,3).size # 3
But what if a subclass doesn't want to use this default? This does not work:
import math class Square_Integers_Below(Math_Set_Base): def __init__(self,cap): self.size = int(math.sqrt(cap)) Square_Integers_Below(7) # Traceback (most recent call last): # File "<stdin>", line 1, in <module> # File "<stdin>", line 3, in __init__ # AttributeError: can't set attribute
I realize there are ways to override a property with a property, but I'd like to avoid that. Because the purpose of the base class is to make life as easy as possible for its user, not to add bloat by imposing a (from the subclass's narrow point of view) convoluted and superfluous access method.
Can it be done? If not what's the next best solution?
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.
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.
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, while self.culture
failedculture
does not exist in Child.__dict__
(the mappingproxy
of the class, not to be confused with the instance __dict__
)culture
exists in c.__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 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:
property
, retrieve its value via getter
or fget
(more on this later). Current class first to Base class last.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.
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!
... 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 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:
property
for this?property
, is there any reason I would need to overwrite it?property
don't apply?property
s, would a separate method serve me better than simply reassigning, since reassigning can accidentally void the property
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With