Search code examples
pythonsuperpython-class

Why does this parent class setter call use type(self) rather than self?


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.


Solution

  • 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.