2

Python @property inheritance the right way explains how to call the parent setter.

class Number:
    def __init__(self):
        self._value = None
    
    @property
    def value(self):
        assert self._value is not None
        return self._value

    @value.setter
    def value(self, new_value):
        self._value = new_value


class Integer(Number):
    @property
    def value(self):
        return super().value

    @value.setter
    def value(self, new_value):
        _value = int(new_value)
        super(Integer, type(self)).value.fset(self, _value) # <----- OK with using type(self)
        # super(Integer, self).value.fset(self, _value)     # <----- Assert error with self
        
i = Integer()
i.value = 1             # cause assertion error with "super(Integer, self)"
print(i.value) 

Problem

With super(Integer, type(self)).value.fset(self, _value) , i.value = 1 invokes the setter as expected.

With super(Integer, self).value.fset(self, _value), i.value = 1 invokes the getter instead of the setter, hence causing the assertion error.

AssertionError                            Traceback (most recent call last)
<ipython-input-8-2c57a07c128d> in <module>
     35 
     36 i = Integer()
---> 37 i.value = 1
     38 print(i.value)

<ipython-input-8-2c57a07c128d> in value(self, new_value)
     32         _value = int(new_value)
     33         #super(Integer, type(self)).value.fset(self, _value)
---> 34         super(Integer, self).value.fset(self, _value)
     35 
     36 i = Integer()

<ipython-input-8-2c57a07c128d> in value(self)
     10     @property
     11     def value(self):
---> 12         assert self._value is not None
     13         return self._value

Question

Please help understand why super(Integer, self).value.fset(self, _value) goes to the getter instead of the setter although calling fset. Reading the documents and articles, it looks to me passing the object self instead of type/class type(self) is the correct way to access the method bound to the instance itself, but it does not work.

super([type[, object-or-type]])

The object-or-type determines the method resolution order to be searched. The search starts from the class right after the type.

For example, if mro of object-or-type is D -> B -> C -> A -> object and the value of type is B, then super() searches C -> A -> object.

The mro attribute of the object-or-type lists the method resolution search order used by both getattr() and super(). The attribute is dynamic and can change whenever the inheritance hierarchy is updated.

Supercharge Your Classes With Python super()

In Python 3, the super(Square, self) call is equivalent to the parameterless super() call. The first parameter refers to the subclass Square, while the second parameter refers to a Square object which, in this case, is self. You can call super() with other classes as well:

    def surface_area(self):
        face_area = super(Square, self).area()
        return face_area * 6

    def volume(self):
        face_area = super(Square, self).area()
        return face_area * self.length 

What about the second parameter? Remember, this is an object that is an instance of the class used as the first parameter. For an example, isinstance(Cube, Square) must return True.

By including an instantiated object, super() returns a bound method: a method that is bound to the object, which gives the method the object’s context such as any instance attributes. If this parameter is not included, the method returned is just a function, unassociated with an object’s context.

John Kugelman
  • 307,513
  • 65
  • 473
  • 519
mon
  • 8,766
  • 4
  • 52
  • 87

1 Answers1

2

The problem with super(Integer, self).value.fset(self, _value) (or the simpler equivalent, super().value.fset(self, _value)) occurs before you even get to the fset. The descriptor protocol is engaged on all lookups on an instance, cause it to invoke the getter simply by doing super(Integer, self).value (or super().value). That's why your inherited getter works in the first place; it invoked the property descriptor, and got the value produced by it.

In order to bypass the descriptor protocol (more precisely, move from instance to class level invocation, where propertys do nothing special in the class level scenario), you need to perform the lookup on the class itself, not an instance of it. super(Integer, type(self)) invokes the form of super that returns a super object bound at the class level, not the instance level, allowing you to retrieve the raw descriptor itself, rather than invoking the descriptor and getting the value it produces. Once you have the raw descriptor, you can access and invoke the fset function attached to it.

This is the same issue you have when super isn't involved. If you have an instance of Number, and want to directly access the fset function (rather than invoking it implicitly via assignment), you have to do:

num = Number()
type(num).value.fset(num, 1)

because doing:

num.value.fset(num, 1)

fails when you retrieve num.value (getting the None the getter produces), then try to look up fset on None.

ShadowRanger
  • 108,619
  • 9
  • 124
  • 184
  • "super(Integer, type(self)) invokes the form of super that returns a super object bound at the class level, not the instance level, allowing you to retrieve the raw descriptor itself, rather than invoking the descriptor and getting the value it produces." - well, it still invokes the descriptor protocol, but the class version of the descriptor protocol, not the instance version. This matters for something like `@classmethod`, but for a property, lookup on a class just returns the property object. – user2357112 supports Monica Mar 07 '21 at 03:47
  • @user2357112supportsMonica: Yeah, I didn't want to get into the weeds on this in the body of the main answer. There's also the case where the *instance* level descriptor protocol is still invoked *if* the class's metaclass defines such a descriptor (because all classes are themselves instances of their metaclass; it's turtles all the way down). I added a parenthetical about your note. – ShadowRanger Mar 07 '21 at 03:49
  • @ShadowRanger, thanks a lot for the answer and sorry for having not accepted yet as I need time to digest the content and the descriptor protocol. – mon Mar 14 '21 at 06:01