Python Object-Oriented Python Inheritance Multiple Superclasses

Ewerton Luna
Ewerton Luna
Full Stack JavaScript Techdegree Student 15,251 Points

This video gave me so many questions...

I'll do my best to make myself clear since English is not my first language.

I think Kenneth took big leap from the previous video to this one.

In the previous video, he teaches us that the super() function lets us call a bit of code from the parent class inside our own class. This is really helpful when you need to override a method from the superclass, defining your own version, but keep the effects of the parent class's version of the code.

When doing so, he says that you have to give some more data to the super function, wich would be the parent's class method you wanna call. When calling the parent's class method, you should also include the required arguments from the parent class, for example:

class Character:
    def __init__(self, name, **kwargs):
        self.name = name

        for key, value in kwargs.items():
            setattr(self, key, value)


class Thief(Character):

    def __init__(self, name, sneaky = True, **kwargs):
        super().__init__(name, **kwargs) # Equal arguments as the parent's class __init__
        self.sneaky = sneaky

I thought I got that one clear. But only I thought....

So, what I got was that if the init from the parent's class has, say, a, b, arguments, when calling it with the super function you also have to include a, b arguments.

The thing that got me very confused was that both Agile and Sneaky classes call the init from a superior class passing *args and **kwargs as arguments.

But in the Character class there is no *args in the arguments. I can't understand why or how this *args are being used to call the super class init

1 Answer

Alex Koumparos
Alex Koumparos
Python Web Development Techdegree Student 33,851 Points

Hi Ewerton,

*args and **kwargs are special 'packed' variables. The video dealing with this is Packing and Unpacking Dictionaries.

I'll try to cover this specifically with reference to the code you're dealing with.

But first, let's go over how Python handles function/method arguments. You can call a Python function or method using a combination of positional arguments and keyword arguments. Here's a simple example:

def my_func(a, b, c)
    print(a)
    print(b)
    print(c)

You can call that function as follows:

my_func(3, 4, 5)  # will print 3 then 4 then 5
my_func(a=9, b=7, c=2)  # will print 9 then 7 then 2
my_func(6, 1, c=4)  # will print 6 then 1 then 4
my_func(b=2, c=3, a=1)  # will print 1, then 2, then 3

The important rule is that you can use both positional arguments and keyword arguments in a function call but you must provide all the positional arguments before any keyword arguments.

So the following call would be invalid syntax:

my_func(c=2, 1, 4)

Python lets you pass arbitrary number of arguments and keyword arguments into a function. If you declare a function like this:

def my_func(a, b, c, *args):
    print(a)
    print(b)
    print(c)
    print(args)

You now have a function that has three required positional arguments (a, b, c) but can take any number of additional positional arguments.

It will treat the additional arguments, that we've named 'args' as a tuple, so if we call the function like this:

my_func(1, 2, 3, 4, 5)

We'll see:

1
2
3
(4, 5)

If that was as far as it went, that would be pretty cool, we could get any values we want out of the tuple args just by indexing into it, e.g., in the above example, args[0] is 4 and args[1] is 5.

The asterix is important because we didn't call that function with a tuple of (4, 5), we called it with independent arguments of 4 and 5. The asterix 'packed' those variables into a tuple called (by convention) 'args'.

We can access the unpacked variables inside the function using *args.

We can do exactly the same thing with keyword arguments, using two asterixes:

def my_func(a, b, c, **kwargs):
    print(a)
    print(b)
    print(c)
    print(kwargs)

The only difference is that since these are keyword arguments, they must be called with argument names:

my_func(1, 2, 3, fourth_value=4, fifth_value=5)

Python turns those keyword arguments into a dictionary, called kwargs where the argument is key and the value is the value. So when called, we see:

1
2
3
{'fourth_value': 4, 'fifth_value': 5}

In the above example, we're packing keyword arguments and values into a dictionary, but inside the function we're just using the dictionary as a dictionary. That's pretty useful but suppose we wanted to call another function from inside our function, we can simply pass the packed keywords with the asterixes (instead of the dictionary) and it would be like calling the function with the original arguments.

Example:

def f(a, b, c, d):
    print(a)
    print(b)
    print(c)
    print(d)


def g(a, **kwargs):
    f(a, **kwargs)

g(1, b=2, c=3, d=4)

Here we've defined f that must take four arguments, a, b, c, and d. We've also defined g that must take a but can take any other keyword arguments. That means if we call g with a positional argument and arguments named b, c, and d, that function can successfully call f. In this case, g is calling f like this:

f(1, b=2, c=3, d=4)

Significantly, note that it is calling f with separate arguments, not like this:

f(1, {'b': 2, 'c': 3, 'd': 4})

Which is what it would look like if the call was f(a, kwargs) (i.e., without the asterixes).

Now we can turn to the __init__s for Character and Thief.

Thief takes name and optionally sneaky, and then any other number of keyword arguments. It then calls the initialiser in Character, passing name (a required argument) and whatever arguments were passed into Thief as **kwargs. After this, it then sets sneaky.

Let's work an example:

my_thief = Thief('Mr. Tap Shoes', sneaky=False, likes_to_dance=True, twirls_baton=True, stabby='very')

Thief's __init__ method will call super to create a Character like this:

Character('Mr. Tap Shoes', likes_to_dance=True, twirls_baton=True, stabby='very')

Then it will take responsibility for setting sneaky:

self.sneaky = sneaky  # which we passed in as False

In all the above examples, we've only used either *args to provide an arbitrary number of positional arguments, OR **kwargs to provide an arbitrary number of keyword arguments. However, it is very common to define functions and methods with both, so that the function can receive any inputs and then pass all those inputs to another function, without knowing or caring which positional and/or keyword arguments it wants.

Hope that clears things up. Let me know if you need any further help.

Cheers

Alex

Ewerton Luna
Ewerton Luna
Full Stack JavaScript Techdegree Student 15,251 Points

Wow, I loved the explanation!!! It was very clear and helpful! Thank you so much for helping and for taking the time to explain this. I had the wrong Idea about what *args and **kwargs meant in the super().init call, but now it makes total sense to me. Again, thanks!!

Andrew Bickham
Andrew Bickham
1,461 Points

I second that thank you!