4

I have a question in Python that seems very complex to me, that combines inheritance, recursion, and the super() function.

First of all, I am using Python 3, and I have a structure of deep inheritance.

In the very first parent class I declare a method, and I want that method to be called from each child class in the hierarchy, but with different inputs for each of them.

The use of that structure seems very pythonic to me and it really saves me from a lot of code repetition.

A simplified sample of my code is shown below:

class ClassA(object):
    def __init__(self):
        self.attr = 'a'


    @classmethod
    def _classname(cls):
        return cls.__name__


    def method1(self):
        if self._classname() != 'ClassA': #don't call it for the very first parent class
            super().method1()
        print(self.attr)

class ClassB(ClassA):
    def __init__(self):
        self.attr = 'b'


class ClassC(ClassB):
    def __init__(self):
        self.attr = 'c'


inst1 = ClassC()
inst1.method1()

I expect that code to print

'a'
'b'
'c'

Instead it raises an attribute error:

super().method1()
AttributeError: 'super' object has no attribute 'method1'

I know that it is a complex problem, but I have tried to divide it. I tried to remove the recursion part, but I do not get any better.

Based on the various attempts I have done, I believe that I am very close to the cause of the problem, and it seems to me like a syntax problem or something that simple.

Thanks!!

Ashlou
  • 684
  • 1
  • 6
  • 21
Xarris Kor
  • 63
  • 1
  • 5

2 Answers2

6

I'm afraid you have built up the wrong mental model on how Python instances and classes relate. Classes only provide a series of attributes for instances to 'inherit', not separate namespaces for instance attributes to live in. When you look up an attribute on an instance and the attribute doesn't exist on the instance itself, a search is done of the classes that back the instance, with the 'nearest' class with that attribute winning over others. super() just lets you reach attributes with the same name but defined on a next class in that same layered search space.

In order for super() to work correctly, Python records what class the method1 function was defined on. Here that's ClassA, and super() will only find attributes on the parent classes of ClassA. In your example, ClassC and ClassB had already been searched and they didn't have a method1 attribute, so ClassA.method1 is being used, but there is no further method1 attribute in the rest of the layers that are searched (only object remains, and there is no object.method1).

You don't need to use super() when subclasses are not overriding a method, nor can you do what you want with super() anyway. Note that the ClassB and ClassC subclasses do not get a copy of the method at all, there is no ClassC.method1 direct attribute that needs to account for ClassB.method1 to exist, etc. Again, what happens when looking up attributes on an instance is that all class objects in the inheritance hierarchy of the instance are inspected for that attribute, in a specific order.

Take a look at your subclasses:

>>> inst1
<__main__.ClassC object at 0x109a9dfd0>
>>> type(inst1)
<class '__main__.ClassC'>
>>> type(inst1).__mro__
(<class '__main__.ClassC'>, <class '__main__.ClassB'>, <class '__main__.ClassA'>, <class 'object'>)

The __mro__ attribute gives you the method resolution order of your ClassC class object; it is this order that attributes are searched for, and that super() uses to further search for attributes. To find inst1.method, Python will step through each of the objects in type(inst1).__mro__ and will return the first hit, so ClassA.method1.

In your example, you used super() in the ClassA.method1() definition. Python has attached some information to that function object to help further searches of attributes:

>>> ClassA.method1.__closure__
(<cell at 0x109a3fee8: type object at 0x7fd7f5cd5058>,)
>>> ClassA.method1.__closure__[0].cell_contents
<class '__main__.ClassA'>
>>> ClassA.method1.__closure__[0].cell_contents is ClassA
True

When you call super() the closure I show above is used to start a search along the type(self).__mro__ sequence, starting at the next object past the one named in the closure. It doesn't matter that there are subclasses here, so even for your inst1 object everything is skipped an only object is inspected:

>>> type(inst1).__mro__.index(ClassA)  # where is ClassA in the sequence?
2
>>> type(inst1).__mro__[2 + 1:]  # `super().method1` will only consider these objects, *past* ClassA
(<class 'object'>,)

At no point are ClassB or ClassC involved here anymore. The MRO depends on the class hierarchy of the current instance, and you can make radical changes when you start using multiple inheritance. Adding in extra classes into a hierarchy can alter the MRO enough to insert something between ClassA and object:

>>> class Mixin(object):
...     def method1(self):
...         print("I am Mixin.method1!")
...
>>> class ClassD(ClassA, Mixin): pass
...
>>> ClassD.__mro__
(<class '__main__.ClassD'>, <class '__main__.ClassA'>, <class '__main__.Mixin'>, <class 'object'>)
>>> ClassD.__mro__[ClassD.__mro__.index(ClassA) + 1:]
(<class '__main__.Mixin'>, <class 'object'>)

ClassD inherits from ClassA and from Mixin. Mixin inherits from object too. Python follows some complicated rules to put all classes in the hierarchy into a logical linear order, and Mixin ends up between ClassA and object because it inherits from the latter, and not the former.

Because Mixin is injected into the MRO after ClassA, calling Class().method1() changes how super().method1() behaves, and suddenly calling that method will do something different:

>>> ClassD().method1()
I am Mixin.method1!
a

Remember, it helps to see classes as a layered search space for attributes on instances! instance.attribute is searched for along the classes if the attribute doesn't exist on the instance itself. super() just let you search for the same attribute along the remainder of that search space.

This lets you reuse method implementations when implementing a method with the same name in a subclass. That's the whole point of super()!

There are other problems with your code.

  • When looking up methods, they are bound to the object they were looked up on. instance.method binds the method to instance, so that when you call instance.method(), Python knows what to pass into the method as self. For classmethod objects, self is replaced with type(self), unless you did ClassObject.attribute, at which point ClassObject is used.

    So your _classname method will always be producing ClassC for inst1, as the cls object that is passed in is that of the current instance. super() doesn't change what class classmethods are bound to when accessed on an instance! It'll always be type(self).

  • You also forgot to call super() in the ClassB and ClassC __init__ methods, so for inst1, only ClassC.__init__ is ever actually used. The ClassB.__init__ and ClassC.__init__ implementations are never called. You'd have to add a call to super().__init__() in both for that to happen, at which point there are three self.attr = ... assignments on the same instance, and only the one that executes last will remain. There is no separate self for each of the classes that make up the code for the instance, so there are no separate self.attr attributes with different values.

    Again, that's because inst1.__init__() is called, __init__ is bound to inst for the self argument, and even if you used super().__init__() the self that is passed on remains inst1.

What you want to achieve is something entirely different from an attribute search across all of the classes. Printing all class names can be done with a loop over __mro__ instead:

class ClassA(object):
    def method2(self):
        this_class = __class__   # this uses the same closure as super()!
        for cls in type(self).__mro__:
            print(cls.__name__)
            if cls is this_class:
                break


class ClassB(ClassA): pass
class ClassC(ClassB): pass

This then produces:

>>> inst1 = ClassC()
>>> inst1.method2()
ClassC
ClassB
ClassA

If you have to print 'c', 'b', 'a' you can add extra attributes to each class:

class ClassA(object):
    _class_attr = 'a'

    def method2(self):
        this_class = __class__   # this uses the same closure as super()!
        for cls in type(self).__mro__:
            if hasattr(cls, '_class_attr'):
                print(cls._class_attr)

class ClassB(ClassA):
    _class_attr = 'b'

class ClassC(ClassB):
    _class_attr = 'c'

and you'll get

c
b
a

printed.

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
0

Thats because you're trying to access Method1() on Object_class

def method1(self):
    # this will print the class name you're calling from, if you are confused.
    print(self._classname()) 
    print(self.attr)

You've written an if-else which satisfies when you're instantialize with Class C and access method1().

To print a,b,c, you will need override the method in every class.. and calling super from them. However, this will still have a problem. Because the self attribute carries the instance of Class C, not Class B or A. So, they can't access the attribute that you're initializing in the init func.

Final code looks like this

class ClassA(object):
    def __init__(self):
        self.attr = 'a'


    @classmethod
    def _classname(cls):
        return cls.__name__


    def method1(self):
        print(self._classname())
        #don't call it for the very first parent class
        #super().method1()
        print(self.attr)

class ClassB(ClassA):
    def __init__(self):
        self.attr = 'b'

    def method1(self):
        print(self._classname())
        print(self.attr)
        super().method1()


class ClassC(ClassB):
    def __init__(self):
       self.attr = 'c'

    def method1(self):
        print(self._classname())
        print(self.attr)
        super().method1()


inst1 = ClassC()
inst1.method1()
O_o
  • 1,103
  • 11
  • 36
  • The *final code* you posted will print `c c c`, because at **all times** the `cls` object that `_classname` is bound to is `ClassC`. `super().method1()` doesn't change the type of `self`! – Martijn Pieters Jun 03 '18 at 19:48