Python 201 - super

I've written about super briefly in the past, but decided to take another go at writing something more interesting about this particular Python function.

The super built-in function was introduced way back in Python 2.2. The super function will return a proxy object that will delegate method calls to a parent or sibling class of type. If that was a little unclear, what it allows you to do is access inherited methods that have been overridden in a class. The super function has two use cases. The first is in single inheritance where super can be used to refer to the parent class or classes without actually naming them explicitly. This can make your code more maintainable in the future. This is similar to the behavior that you will find in other programming languages, like Dylan's *next-method*.

The second use case is in a dynamic execution environment where super supports cooperative multiple inheritance. This is actually a pretty unique use case that may only apply to Python as it is not found in languages that only support single inheritance nor in statically compiled languages.

super has had its fair share of controversy even among core developers. The original documentation was confusing and using super was tricky. There were some who even labeled super as harmful, although that article seems to apply more to the Python 2 implementation of super then the Python 3 version. We will start out this chapter by looking at how to call super in both Python 2 and 3. Then we will learn about Method Resolution Order.


Python 2 vs Python 3

Let's start by looking at a regular class definition. Then we'll add super using Python 2 to see how it changes.

class MyParentClass(object):
    def __init__(self):
        pass

class SubClass(MyParentClass):
    def __init__(self):
        MyParentClass.__init__(self)

This is a pretty standard set up for single inheritance. We have a base class and then the subclass. Another name for base class is parent class or even super class. Anyway, in the subclass we need to initialize the parent class too. The core developers of Python thought it would be a good idea to make this kind of thing more abstract and portable, so the super function was added. In Python 2, the subclass would look like this:

class SubClass(MyParentClass):
    def __init__(self):
        super(SubClass, self).__init__()

Python 3 simplified this a bit. Let's take a look:

class MyParentClass():
    def __init__(self):
        pass

class SubClass(MyParentClass):
    def __init__(self):
        super().__init__()

The first change you will notice is that the parent class no longer needs to be explicitly based on the object base class. The second change is the call to super. We no longer need to pass it anything and yet super does the right thing implicitly. Most classes actually have arguments passed to them though, so let's look at how the super signature changes in that case:

class MyParentClass():
    def __init__(self, x, y):
        pass

class SubClass(MyParentClass):
    def __init__(self, x, y):
        super().__init__(x, y)

Here we just need to call the super's __init__ method and pass the arguments along. It's still nice and straight-forward.


Method Resolution Order (MRO)

Method Resolution Order (MRO) is just a list of types that the class is derived from. So if you have a class that inherits from two other classes, you might think that it's MRO will be itself and the two parents it inherits from. However the parents also inherit from Python's base class: **object**. Let's take a look at an example that will make this clearer:

class X:
    def __init__(self):
        print('X')
        super().__init__()

class Y:
    def __init__(self):
        print('Y')
        super().__init__()

class Z(X, Y):
    pass


z = Z()
print(Z.__mro__)

Here we create 3 classes. The first two just print out the name of the class and the last one inherits from the previous two. Then we instantiate the class and also print out its MRO:

X
Y
(, , , )

As you can see, when you instantiate it, each of the parent classes prints out its name. Then we get the Method Resolution Order, which is ZXY and object. Another good example to look at is to see what happens when you create a class variable in the base class and then override it later:

class Base:
    var = 5
    def __init__(self):
        pass

class X(Base):
    def __init__(self):
        print('X')
        super().__init__()

class Y(Base):
    var = 10
    def __init__(self):
        print('Y')
        super().__init__()

class Z(X, Y):
    pass


z = Z()
print(Z.__mro__)
print(super(Z, z).var)

So in this example, we create a Base class with a class variable set to 5. Then we create subclasses of our Base class: X and Y. Y overrides the Base class's class variable and sets it to 10. Finally we create class Z which inherits from X and Y. When we call super on class Z, which class variable will get printed? Try running this code and you'll get the following result:

X
Y
(, , , , )
10

Let's parse this out a bit. Class Z inherits from X and Y. So when we ask it what it's var is, the MRO will look at X to see if it is defined. It's not there, so it moves on to Y. Y has it, so that is what gets returned. Try adding a class variable to X and you will see that it overrides Y because it is first in the Method Resolution Order.

There is a wonderful document that a fellow named Michele Simionato created that describes Python's Method Resolution Order in detail. You can check it out here: https://www.python.org/download/releases/2.3/mro/. It's a long read and you'll probably need to re-read portions of it a few times, but it explains MRO very well. By the way, you might note that this article is labeled as being for Python 2.3, but it still applies even in Python 3, even though the calling of super is a bit different now.

The super method was updated slightly in Python 3. In Python 3, super can figure out what class it is invoked from as well as the first argument of the containing method. It will even work when the first argument is not called self! This is what you see when you call super() in Python 3. In Python 2, you would need to call super(ClassName, self), but that is simplified in Python 3.

Because of this fact, super knows how to interpret the MRO and it stores this information in the following magic propertie: __thisclass__ and __self_class__. Let's look at an example:

class Base():
    def __init__(self):
        s = super()
        print(s.__thisclass__)
        print(s.__self_class__)
        s.__init__()

class SubClass(Base):
    pass

sub = SubClass()

Here we create a base class and assign the super call to a variable so we can find out what those magic properties contain. Then we print them out and initialize. Finally we create a SubClass of the Base class and instantiate the SubClass. The result that gets printed to stdout is this:



This is pretty cool, but probably not all that handy unless you're doing a metaclasses or mixin classes.


Wrapping Up

There are lots of interesting examples of super that you will see on the internet. Most of them tend to be a bit mind-bending at first, but then you'll figure them out and either think it's really cool or wonder why you'd do that. Personally I haven't had a need for super in most of my work, but it can be useful for forward compatibility. What this means is that you want your code to work in the future with as few changes as possible. So you might start off with single inheritance today, but a year or two down the road, you might add another base class. If you use super correctly, then this will be a lot easier to add. I have also seen arguments for using super for dependency injection, but I haven't seen any good, concrete examples of this latter use case. It's a good thing to keep in mind though.

The super function can be very handy or it can be really confusing or a little bit of both. Use it wisely and it will serve you well.


Further Reading

Copyright © 2024 Mouse Vs Python | Powered by Pythonlibrary