24

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?

Paul Panzer
  • 51,835
  • 3
  • 54
  • 99
  • 3
    This likely indicates a problem with the class hierarchy itself. The best solution then is to rework that hierarchy. You mention that *"there is a plausible default implementation for some derived properties"* with the example of math sets and `len(self.elements)` as an implementation. This very specific implementation imposes a contract on all instances of the class, namely that they provide `elements` which is a *sized container*. However your `Square_Integers_Below` class doesn't seem to behave that way (perhaps it's generating its members dynamically), so it must define its own behavior. – a_guest May 04 '20 at 21:25
  • 1
    Overriding is only necessary because it inherited the (wrong) behavior in the first place. `len(self.elements)` is not a suitable default implementation for math sets since there exist "many" sets that don't even have finite cardinality. In general, if a subclass doesn't want to use the behavior from its base classes, it needs to override it at the class level. Instance attributes, as the name suggests, work at the instance level and are hence governed by class behavior. – a_guest May 04 '20 at 21:26

8 Answers8

14

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:

  1. self.world_view was able to be applied, while self.culture failed
  2. culture does not exist in Child.__dict__ (the mappingproxy of the class, not to be confused with the instance __dict__)
  3. Even though 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, 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:

  1. If a class attribute exists and it is a property, retrieve its value via getter or fget (more on this later). Current class first to Base class last.
  2. Otherwise, if an instance attribute exist, retrieve the instance attribute value.
  3. 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:

  1. Do I really need a property for this?
  2. Could a method serve me any differently?
  3. If I need a property, is there any reason I would need to overwrite it?
  4. Does the subclass really belong in the same family if these property don't apply?
  5. If I do need to overwrite any/all propertys, would a separate method serve me better than simply reassigning, since reassigning can accidentally void the propertys?

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.

Community
  • 1
  • 1
r.ook
  • 13,466
  • 2
  • 22
  • 39
13

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.

Arkelis
  • 913
  • 5
  • 12
  • This s pretty close to perfect. So this means a poperty even if you only define the getter does implicitly add a setter that does nothing apart from blocking assignment? Interesting. I'll probably accept this answer. – Paul Panzer Apr 29 '20 at 05:03
  • Yes, according to the docs, a property always defines all three descriptor methods (`__get__()`, `__set__()` and `__delete__()`) and they raise an `AttributeError` if you do not provide any function for them. See [Python equivalent of property implementation](https://docs.python.org/3/howto/descriptor.html#properties). – Arkelis Apr 29 '20 at 09:12
  • 1
    As long as you don't care for the `setter` or `__set__` of the property, this will work. However I would caution you that this also means you can easily overwrite the property not just at class level (`self.size = ...`) but even in instance level (e.g. `Concrete_Math_Set(1, 2, 3).size = 10` would be equally valid). Food for thoughts :) – r.ook Apr 29 '20 at 18:27
  • And `Square_Integers_Below(9).size = 1` is also valid as it is a simple attribute. In this particular "size" use case, this may seem akward (use a property instead), but in the general case "override a computed prop easily in subclass", it might be good in some cases. You can also control attribute access with `__setattr__()` but it could be overwhelming. – Arkelis Apr 29 '20 at 18:42
  • 2
    I'm not disagreeing there might be valid use case for these, I just wanted to mention the caveat as it can complicate debugging efforts in the future. As long as both you and OP are aware of the implications then all is fine. – r.ook Apr 29 '20 at 18:56
9

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.

Green Cloak Guy
  • 23,793
  • 4
  • 33
  • 53
  • Thanks, that's refreshingly simple. Does it mean that even if there never were any properties anywhere in my class and its ancestors it still will first search through the entire inheritance tree for a class level name before it even bothers looking at the `__dict__`? Also, are there any ways to automate this? Yes, it's just one line but it's the kind of thing that is very cryptic to read if you are not familiar with the gory details of proprties etc. – Paul Panzer Apr 19 '20 at 05:31
  • @PaulPanzer I haven't looked into the procedures behind this, so I couldn't give you a satisfactory answer myself. You can try to puzzle it out from [the cpython source code](https://github.com/python/cpython) if you want. As for automating the process, I don't think there's a good way to do that other than either not making it a property in the first place, or just adding a comment/docstring to let those reading your code know what you're doing. Something like `# declare size to not be a @property` – Green Cloak Guy Apr 19 '20 at 22:14
7

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
chepner
  • 497,756
  • 71
  • 530
  • 681
  • That's a great point, just override a parent property with a child property! +1 – r.ook Apr 28 '20 at 22:24
  • 1
    This is in a way the straight-forward thing to do, but I was interested in and did ask specifically for ways tio allow a non property override so as not to impose the inconvenience of having to write a memoizing property where simple assignment would do the job. – Paul Panzer Apr 29 '20 at 04:54
  • I'm not sure why you would want to complicate your code like that. For the small price of recognizing that `size` is a property and not an instance attribute, you don't have to do *anything* fancy. – chepner Apr 29 '20 at 11:51
  • 1
    My use case is lots of child classes with lots of attributes; not having to write 5 lines instead of one for all those justifies a certain amount of fancicity, IMO. – Paul Panzer Apr 29 '20 at 17:44
2

It looks like you want to define size in your class:

class Square_Integers_Below(Math_Set_Base):
    size = None

    def __init__(self, cap):
        self.size = int(math.sqrt(cap))

Another option is to store cap in your class and calculate it with size defined as a property (which overrides the base class's property size).

Uri
  • 2,992
  • 8
  • 43
  • 86
2

I suggest adding a setter like so:

class Math_Set_Base:
    @property
    def size(self):
        try:
            return self._size
        except:
            return len(self.elements)

    @size.setter
    def size(self, value):
        self._size = value

This way you can override the default .size property like so:

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(7).size) # 2
Amir
  • 1,871
  • 1
  • 12
  • 10
1

Also you can do next thing

class Math_Set_Base:
    _size = None

    def _size_call(self):
       return len(self.elements)

    @property
    def size(self):
        return  self._size if self._size is not None else self._size_call()

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))
Ryabchenko Alexander
  • 10,057
  • 7
  • 56
  • 88
  • That's neat, but you don't really need `_size` or `_size_call` there. You could have baked in the function call within `size` as a condition, and use `try... except... ` to test for `_size` instead of taking an extra class reference that won't get used. Regardless though, I feel this is even more cryptic than the simple `size = None` overwriting in the first place. – r.ook Apr 28 '20 at 14:36
1

I think it is possible to set a property of a base class to another property from a derived class, inside the derived class and then to use the property of the base class with a new value.

In the initial code there is a sort of conflict between the name size from the base class and the attribute self.size from the derived class. This can be visible if we replace the name self.size from the derived class with self.length. This will output:

3
<__main__.Square_Integers_Below object at 0x000001BCD56B6080>

Then, if we replace the name of the method size with length in all occurrences throughout the program, this will result in the same exception:

Traceback (most recent call last):
  File "C:/Users/Maria/Downloads/so_1.2.py", line 24, in <module>
    Square_Integers_Below(7)
  File "C:/Users/Maria/Downloads/so_1.2.py", line 21, in __init__
    self.length = int(math.sqrt(cap))
AttributeError: can't set attribute

The fixed code, or anyway, a version that somehow works, is to keep the code exactly the same, with the exception of the class Square_Integers_Below, which will set the method size from the base class, to another value.

class Square_Integers_Below(Math_Set_Base):

    def __init__(self,cap):
        #Math_Set_Base.__init__(self)
        self.length = int(math.sqrt(cap))
        Math_Set_Base.size = self.length

    def __repr__(self):
        return str(self.size)

And then, when we run all of the program, the output is:

3
2

I hope this was helpful in one way or another.

Sofia190
  • 53
  • 7