4

As the following example shows, super() has some strange (at least to me) behavior when used in diamond inheritance.

class Vehicle:
    def start(self):
        print("engine has been started")

class LandVehicle(Vehicle):
    def start(self):
        super().start()
        print("tires are safe to use")

class WaterCraft(Vehicle):
    def start(self):
        super().start()
        print("anchor has been pulled up")

class Amphibian(LandVehicle, WaterCraft):
    def start(self):
        # we do not want to call WaterCraft.start, amphibious
        # vehicles don't have anchors
        LandVehicle.start(self)
        print("amphibian is ready for travelling on land")

amphibian = Amphibian()
amphibian.start()

The above code produces the following output:

engine has been started
anchor has been pulled up
tires are safe to use
amphibian is ready for travelling on land

When I call super().some_method(), I would never expect a method of a class on the same inheritance level to be called. So in my example, I would not expect anchor has been pulled up to appear in the output.

The class calling super() may not even know about the other class whose method is eventually called. In my example, LandVehicle may not even know about WaterCraft.

Is this behavior normal/expected and if so, what is the rationale behind it?

user3738870
  • 1,415
  • 2
  • 12
  • 24
  • 2
    That behavior is most of the point of Python's `super`. (Yes, the name is confusing.) – user2357112 Sep 26 '18 at 21:49
  • 1
    Read this: [super considered super](https://rhettinger.wordpress.com/2011/05/26/super-considered-super/) – Daniel Roseman Sep 26 '18 at 21:52
  • So I should never user `super` for calling base class method's? What is it good for then, why is it a good thing that it can also go down the inheritance chain? – user3738870 Sep 26 '18 at 21:53
  • Oddly enough, I can't find a duplicate for this question... but [this answer](https://stackoverflow.com/a/33469090/1222951) explains the behavior you've observed. – Aran-Fey Sep 26 '18 at 21:56
  • Because that's the whole point! If you don't want the methods to be called why inherit from the class? – Daniel Roseman Sep 26 '18 at 21:57
  • @DanielRoseman I may want to use other behavior of the class, but inside `start` method, I would just want to use the other base class method, so that's why I call the method belonging to that other class explicitly (`LandVehicle.start(self)`) instead of using `super()` – user3738870 Sep 26 '18 at 21:59
  • 1
    [This](https://stackoverflow.com/questions/45390553/calling-super-class-method-in-multiple-inheritance) is technically a duplicate, but I like neither the question nor the answer very much. I think we can do better than that. – Aran-Fey Sep 26 '18 at 22:09
  • 4
    If an `Amphibian` doesn't have an anchor, then either it should not inherit from `WaterCraft`, or `WaterCraft` should allow for an *optional* anchor. – chepner Sep 26 '18 at 22:25
  • @chepner Let's suppose the implementation of `start` in `Watercraft` is a default implementation on the premise that most watercrafts have anchors, but the ones that don't can override the method. This is exactly what `Amphibian` is trying to accomplish. How is it supposed to know that in some way, its overridden base method will still get called (as a result of calling the same method of another base class) ? – user3738870 Sep 26 '18 at 22:35

1 Answers1

3

When you use inheritance in Python, each class defines a Method Resolution Order (MRO) that is used to decide where to look when a class attribute is looked up. For your Amphibian class, for instance, the MRO is Amphibian, LandVehicle, WaterCraft, Vehicle and finally object. (You can see this for yourself by calling Amphibian.mro().)

The exact details of how the MRO is derived are a little complicated (though you can find a description of how it works if you're interested). The important thing to know is that any child class is always listed before its parent classes, and if multiple inheritance is going on, all the parents of a child class will be in the same relative order they are in the class statement (other classes may appear in between the parents, but they'll never be reversed relative to one another).

When you use super to call an overridden method, it looks though the MRO like it does for any attribute lookup, but it starts its search further along than usual. Specifically, it starts to search for an attribute just after the "current" class. By "current" I mean, the class containing the method in which the super call (even if the object the method is being called on is of some other more derived class). So when LandVehicle.__init__ calls super().__init__, it begins checking for an __init__ method in the the first class after LandVehicle in the MRO, and finds WaterCraft.__init__.

This suggests one way you could fix the issue. You could have Amphibian name WaterCraft as its first base class, and LandVehicle second:

class Amphibian(Watercraft, LandVehicle):
    ...

Changing the order of the bases will also change their order in the MRO. When Amphibian.__init__ calls LandVehicle.__init__ directly by name (rather than using super), the subsequent super calls will skip over WaterCraft since the class they're being called from is already further along in the MRO. Thus the rest of the super calls will work as you intended.

But that's not really a great solution. When you explicitly name a base class like that, you may find that it breaks things later if you have your more child classes that want to do things differently. For instance, a class derived from the reordered-base Amphibian above might end up with other base classes in between WaterCraft and LandVehcle, which would also have their __init__ methods skipped accidentally when Amphibian.__init__ calls LandVehcle.__init__ directly.

A better solution would be to allow all the __init__ methods to get called in turn, but to factor out the parts of them you might not want to always run into other methods that can be separately overridden.

For example, you could change WaterCraft to:

class WaterCraft(Vehicle):
    def start(self):
        super().start()
        self.weigh_anchor()

    def weigh_anchor(self):
        print("anchor has been pulled up")

The Amphibian class could override the anchor specific behavior (e.g. to do nothing):

class Amphibian(LandVehicle, WaterCraft):
    def start(self):
        super().start(self)
        print("amphibian is ready for travelling on land")

    def weigh_anchor(self):
        pass # no anchor to weigh, so do nothing

Of course in this specific case, where WaterCraft doesn't do anything other than raise its anchor, it would be even simpler to remove WaterCraft as a base class for Amphibian. But the same idea can often work for non-trivial code.

Blckknght
  • 100,903
  • 11
  • 120
  • 169
  • Okay, thanks for the detailed answer. I really like the solution that the method should be factored out. My only remaining worry is about cases where you don't have access to the base classes and so can't apply such changes to it. (In that case, the other solution may still work though, but what if it can't be used either...) – user3738870 Sep 26 '18 at 23:43