Superclass property setting using super() and multiple inheritance

78 views Asked by At

In a real world program I have ran into the next problem: I have a diamond inheritance having SuperClass, MidClassA, MidClassB and SubClass. SuperClass has a property (a filename, actually) that is used by its successors, but different ways (with or without extension). At all levels I want to set this property by a setter. Here is an example code:

class SuperClass:
    def __init__(self):
        print("SuperClass __init__")
        super().__init__()

    @property
    def value(self):
        return self._value

    @value.setter
    def value(self, new_value):
        self._value = new_value
        print("Superclass setter called!")


class MidClassA(SuperClass):
    def __init__(self):
        print("MidClassA __init__")
        super().__init__()

    @SuperClass.value.setter
    def value(self, new_value):
        super(MidClassA, MidClassA).value.__set__(self, new_value)
        print("MidClassA setter called!", MidClassA.__mro__)


class MidClassB(SuperClass):
    def __init__(self):
        print("MidClassB __init__")
        super().__init__()

    @SuperClass.value.setter
    def value(self, new_value):
        super(MidClassB, MidClassB).value.__set__(self, new_value)
        print("MidClassB setter called!", MidClassB.__mro__)


class SubClass(MidClassB, MidClassA):
    def __init__(self):
        print("SubClass __init__")
        super().__init__()

    @SuperClass.value.setter
    def value(self, new_value):
        super(SubClass, SubClass).value.__set__(self, new_value)
        print("Subclass setter called!", SubClass.__mro__)


obj = SubClass()
obj.value = 42
print(obj.value)

And an ouput:

SubClass __init__
MidClassB __init__
MidClassA __init__
SuperClass __init__
Superclass setter called!
MidClassB setter called! (<class '__main__.MidClassB'>, <class '__main__.SuperClass'>, <class 'object'>)
Subclass setter called! (<class '__main__.SubClass'>, <class '__main__.MidClassB'>, <class '__main__.MidClassA'>, <class '__main__.SuperClass'>, <class 'object'>)
42

Questions:

  • As output displays, the MRO of __init__s works well (SubClass-MidClassB-MidClassA-SuperClass-object) but during the propery setting MidClassA is skipped.
  • How to get rid of the super(MidClassB, MidClassB)?
  • Is it possible to get rid of the __set__ method?

An ideal solution would be something like super().value = new_value, but all these don't work.

2

There are 2 answers

0
Gyula Sámuel Karli On

Just after posting this I realized that two of the three above mentioned questions has a good solution:

super(MidClassB, MidClassB) is a mistaken expression as it "positions" to the same class in MRO regarding the actual class of the object. So the proper designation is super(MidClassB, self.__class__). This will return the proper class those set method will be called.

The only question left is whether it is possible to have an instance setter instead of this classmethod __set__(cls, instance, value).

So a working piece of code is here:

class SuperClass:
    def __init__(self):
        print("SuperClass __init__")
        super().__init__()

    @property
    def value(self):
        return self._value

    @value.setter
    def value(self, new_value):
        self._value = new_value
        print("Superclass setter called!")


class MidClassA(SuperClass):
    def __init__(self):
        print("MidClassA __init__")
        super().__init__()

    @SuperClass.value.setter
    def value(self, new_value):
        super(MidClassA, self.__class__).value.__set__(self, new_value)
        print("MidClassA setter called!", MidClassA.__mro__)


class MidClassB(SuperClass):
    def __init__(self):
        print("MidClassB __init__")
        super().__init__()

    @SuperClass.value.setter
    def value(self, new_value):
        super(MidClassB, self.__class__).value.__set__(self, new_value)
        print("MidClassB setter called!", MidClassB.__mro__)


class SubClass(MidClassB, MidClassA):
    def __init__(self):
        print("SubClass __init__")
        super().__init__()

    @SuperClass.value.setter
    def value(self, new_value):
        super(SubClass, self.__class__).value.__set__(self, new_value)
        print("Subclass setter called!", SubClass.__mro__)


obj = SubClass()
obj.value = 42
print(obj.value)

and an output:

SubClass __init__
MidClassB __init__
MidClassA __init__
SuperClass __init__
Superclass setter called!
MidClassA setter called! (<class '__main__.MidClassA'>, <class '__main__.SuperClass'>, <class 'object'>)
MidClassB setter called! (<class '__main__.MidClassB'>, <class '__main__.SuperClass'>, <class 'object'>)
Subclass setter called! (<class '__main__.SubClass'>, <class '__main__.MidClassB'>, <class '__main__.MidClassA'>, <class '__main__.SuperClass'>, <class 'object'>)
42

PS as far as I understand super() is an important and practical, yet heavily underdocumented function in Python. It takes two parameters.

  • The first one must be a class from among the caller object's parents and this marks where the search begins for the method: the first class to be searched is the direct parent of the class mentioned in the parameter (actually the next one in the Method Resolution Order). By default this is the class where super() is called.
  • The second parameter is a class or an instance and this defines the object which the aforementioned method will be called upon, ie. this object will be passed to the method as the first parameter. For instance parameters in 99% of the cases this is the self (and this is the default value of this parameter), while for class parameters this should be self.__class__ but I haven't seen this being mentioned anywhere. Interestingly enough, however, for class methods, like __new__(), this second parameters defaults to cls, so using super() within a class method without parameters is meaningful.
0
chepner On

Consider leaving the property alone, and instead overriding a regular instance method used by the setter.

class SuperClass:
    def __init__(self):
        print("SuperClass __init__")
        super().__init__()

    @property
    def value(self):
        return self._value

    @value.setter
    def value(self, new_value):
        self._value_setter(new_value)

    def _value_setter(self, new_value):
        print("Superclass setter called!")
        self._value = new_value


class MidClassA(SuperClass):
    def __init__(self):
        print("MidClassA __init__")
        super().__init__()

    def _value_setter(self, new_value):
        print("MidClassA setter called!", MidClassA.__mro__)
        super()._value_setter(new_value)


class MidClassB(SuperClass):
    def __init__(self):
        print("MidClassB __init__")
        super().__init__()

    def _value_setter(self, new_value):
        print("MidClassB setter called!", MidClassB.__mro__)
        super()._value_setter(new_value)


class SubClass(MidClassB, MidClassA):
    def __init__(self):
        print("SubClass __init__")
        super().__init__()

    def _value_setter(self, new_value):
        print("Subclass setter called!", SubClass.__mro__)
        super()._value_setter(new_value)


obj = SubClass()
obj.value = 42
print(obj.value)