Friday, February 16, 2018

Understanding the super() function in Python

In this blog post, I will explain how the super() function in Python works. The super function is a built-in Python function and can be used within a class to gain access to inherited methods from a parent class that has been overwritten.

So let's look at an example. Assume we want to build a dictionary class which has all properties of dict, but additionally allows us to write to logger. This can be done by defining a class which inherits from the dict class and overwrites the functions with new functions which do the same as the old, but additionally have the logging call. The new dict class would look like this
class LoggingDict(dict):
    def __setitem__(self, key, value):
        logging.info("Setting key %s to %s" % (key, value))
        super().__setitem__(key, value)
    def __getitem__(self, key):
        logging.info("Access key %s" % key)
        super().__getitem__(key)
Here we overwrite the __getitem__ and __setitem__ methods with new ones, but we just want to add a logging functionality and keep the functionality of the original functions. Note that we do not need super() to do this since we could get the same result with
class LoggingDict(dict): 
    def __setitem__(self, key, value): 
        logging.info("Setting key %s to %s" % (key, value))
        dict.__setitem__(self, key, value)
    def __getitem__(self, key): 
        logging.info("Access key %s" % key)
        dict.__getitem__(self, key)
The advantage of super() is that, should you decide that you want to inherit from a different class, you would only need to change the first line of the class definition, while the explicit use of the parent class requires you to go through the entire class and change the parent class name everywhere, which can become quite cumbersome for large classes. So super() makes your code more maintainable.

However, the functionality of super() gets more complicated if you inherit from multiple classes and if the function you refer to is present in more than one of these parent classes. Since the parent class is not explicitly declared, which parent class is addressed by super()?

The super() function considers an order of the inherited classes and goes through that ordered list until it finds the first match. The ordered list is known as the Method Resolution Order (MRO). You can see the MRO of any class as
>>> dict.__mro__
(<type 'dict'>, <type 'object'>)
The use of the MRO in the super() function can lead to very different results in the case of multiple inheritances, compared to the explicit declaration of the parent class. Let's go through another example where we define a Bird class which represents the parent class for the Parrot class and the Hummingbird class:
class Bird(object): 
    def __init__(self): 
        print("Bird init") 

class Parrot(Bird):
    def __init__(self):
        print("Parrot init")
        Bird.__init__(self) 

class Hummingbird(Bird):
    def __init__(self): 
        print("Hummingbird init")
        super(Hummingbird, self).__init__()
Here we used the explicit declaration of the parent class in the Parrot class, while in the Hummingbird class we use super(). From this, I will now construct an example where the Parrot and Hummingbird classes will behave differently because of the super() function.

Let's create a FlyingBird class which handles all properties of flying birds. Non-flying birds like ostriches would not inherit from this class:
class FlyingBird(Bird):
    def __init__(self):
        print("FlyingBird init")
        super(FlyingBird, self).__init__()
Now we produce child classes of Parrot and Hummingbird, which specify specific types of these animals. Remember, Hummingbird uses super, Parrot does not:
class Cockatoo(Parrot, FlyingBird):
    def __init__(self):
        print("Cockatoo init")
        super(Cockatoo, self).__init__()

class BeeHummingbird(Hummingbird, FlyingBird):
    def __init__(self):
        print("BeeHummingbird init")
        super(BeeHummingbird, self).__init__()
If we now initiate an instance of Cockatoo we will find that it will not call the __init__ function of the FlyingBird class
>>> Cockatoo() 
Cockatoo init 
Parrot init 
Bird init 
while an initiation of a BeeHummingbird instance does
>>> BeeHummingbird()
BeeHummingbird init 
Hummingbird init 
FlyingBird init 
Bird init 
To understand the order of calls you might want to look at the MRO
>>> print(BeeHummingbird.__mro__)
(<class 'BeeHummingbird'>, <class 'Hummingbird'>, <class 'FlyingBird'>,
<class 'Bird'>, <type 'object'>)
This is an example where not using super() is causing a bug in the class initiation since all our Cockatoo instances will miss the initiation functionality of the FlyingBird class. It clearly demonstrates that the use of super() goes beyond just avoiding explicit declarations of a parent class within another class.

Just as a side note before we finish, the syntax for the super() function has changed between Python 2 and Python 3. While the Python 2 version requires an explicit declaration of the arguments, as used in this post, Python 3 now does all this implicitly, which changes the syntax from (Python 2)
super(class, self).method(args)
to
super().method(args)
I hope that was useful. Let me know if you have any comments/questions. Note that there are very useful discussions of this topic on stack-overflow and in this blog post.
cheers
Florian

No comments:

Post a Comment