Search code examples
pythoninheritanceselfsupermethod-resolution-order

Python self and super in multiple inheritance


In Raymond Hettinger's talk "Super considered super speak" at PyCon 2015 he explains the advantages of using super in Python in multiple inheritance context. This is one of the examples that Raymond used during his talk:

class DoughFactory(object):
    def get_dough(self):
        return 'insecticide treated wheat dough'


class Pizza(DoughFactory):
    def order_pizza(self, *toppings):
        print('Getting dough')
        dough = super().get_dough()
        print('Making pie with %s' % dough)
        for topping in toppings:
            print('Adding: %s' % topping)


class OrganicDoughFactory(DoughFactory):
    def get_dough(self):
        return 'pure untreated wheat dough'


class OrganicPizza(Pizza, OrganicDoughFactory):
    pass


if __name__ == '__main__':
    OrganicPizza().order_pizza('Sausage', 'Mushroom')

Somebody in the audience asked Raymond about the difference of using self.get_dough() instead super().get_dough(). I didn't understand very well the brief answer of Raymond but I coded the two implementations of this example to see the differences. The output are the same for both cases:

Getting dough
Making pie with pure untreated wheat dough
Adding: Sausage
Adding: Mushroom

If you alter the class order from OrganicPizza(Pizza, OrganicDoughFactory) to OrganicPizza(OrganicDoughFactory, Pizza) using self.get_dough(), you will get this result:

Making pie with pure untreated wheat dough

However if you use super().get_dough() this is the output:

Making pie with insecticide treated wheat dough

I understand the super() behavior as Raymond explained. But what is the expected behavior of self in multiple inheritance scenario?


Solution

  • Just to clarify, there are four cases, based on changing the second line in Pizza.order_pizza and the definition of OrganicPizza:

    1. super(), (Pizza, OrganicDoughFactory) (original): 'Making pie with pure untreated wheat dough'
    2. self, (Pizza, OrganicDoughFactory): 'Making pie with pure untreated wheat dough'
    3. super(), (OrganicDoughFactory, Pizza): 'Making pie with insecticide treated wheat dough'
    4. self, (OrganicDoughFactory, Pizza): 'Making pie with pure untreated wheat dough'

    Case 3. is the one that's surprised you; if we switch the order of inheritance but still use super, we apparently end up calling the original DoughFactory.get_dough.


    What super really does is ask "which is next in the MRO (method resolution order)?" So what does OrganicPizza.mro() look like?

    • (Pizza, OrganicDoughFactory): [<class '__main__.OrganicPizza'>, <class '__main__.Pizza'>, <class '__main__.OrganicDoughFactory'>, <class '__main__.DoughFactory'>, <class 'object'>]
    • (OrganicDoughFactory, Pizza): [<class '__main__.OrganicPizza'>, <class '__main__.OrganicDoughFactory'>, <class '__main__.Pizza'>, <class '__main__.DoughFactory'>, <class 'object'>]

    The crucial question here is: which comes after Pizza? As we're calling super from inside Pizza, that is where Python will go to find get_dough*. For 1. and 2. it's OrganicDoughFactory, so we get the pure, untreated dough, but for 3. and 4. it's the original, insecticide-treated DoughFactory.


    Why is self different, then? self is always the instance, so Python goes looking for get_dough from the start of the MRO. In both cases, as shown above, OrganicDoughFactory is earlier in the list than DoughFactory, which is why the self versions always get untreated dough; self.get_dough always resolves to OrganicDoughFactory.get_dough(self).


    * I think that this is actually clearer in the two-argument form of super used in Python 2.x, which would be super(Pizza, self).get_dough(); the first argument is the class to skip (i.e. Python looks in the rest of the MRO after that class).