4

I'm trying to learn and understand how to use super in Python, Ive been following the book 'Python journey from novice to expert' and although I feel that I understand the concept Im having problems executing super in my own code.

For example, this method works for me:

class Employee:        
    def __init__(self, firstname, lastname, age, sex, dob):
        self.firstname = firstname
        self.lastname = lastname
        self.age = age 
        self.sex = sex
        self.dob = dob
        self.all_staff.append(self)

class Hourly(Employee):
    def __init__(self, firstname, lastname, age, sex, dob, rate, hours):
        self.rate = rate
        self.hours = hours
        super().__init__(firstname, lastname, age, sex, dob)

    def __str__(self):
    return "{} {}\nAge: {}\nSex: {}\nDOB: {}\n".format(self.firstname, self.lastname, self.age, 
        self.sex, self.dob)

    def get_rate(self):
        print('The hourly rate of {} is {} '.format(self.firstname, self.rate))

hourlystaff1 = Hourly('Bob', 'Foo', '23', 'M', '12/1/1980', '$15', '30')

print(hourlystaff1)

print(hourlystaff1.get_rate())

returns the following:

Bob Foo
Age: 23
Sex: M
DOB: 12/1/1980

The hourly rate of Bob is $15 
None

This is what I expected (I'm not sure why 'None' is also being returned though, perhaps someone can explain?).

And then I wanted to try this using super but with **kwargs like so:

class Employee:
    def __init__(self, firstname='', lastname='', age='', dob='', **kwargs):
        super().__init__(**kwargs)
        self.firstname = firstname
        self.lastname = lastname
        self.age = age 
        self.dob = dob 

class Hourly(Employee):

    def __init__(self, rate=''):
        self.rate = rate
        super().__init__(**kwargs)

    def __str__(self):
        return "{} {}\nAge: {}\nSex: {}".format(self.firstname, self.lastname, self.age, 
            self.sex, self.dob, self.rate)

    def get_rate(self):
        print('The hourly rate of {} is {} '.format(self.firstname, self.rate))

bob = Hourly('Bob', 'Bar', '23', '12/1/2019')


bob.get_rate('$12')

returns this error:

  File "staff_b.py", line 33, in <module>
    bob = Hourly('Bob', 'Bar', '23', '12/1/2019')
TypeError: __init__() takes from 1 to 2 positional arguments but 5 were given

what am I doing wrong in this second approach? How can I use **kwargs and super correctly here?

Edit:

this is a screenshot of an example from the book which I have been following:

enter image description here

what is different between how I use **kwargs and super in my second example to there?

This is also a comprehensive case study from the same book and chapter. This works for me and I understand how it works but I dont seem to be able to translate it into my own work.

https://pastebin.com/NYGJfMik

smci
  • 32,567
  • 20
  • 113
  • 146
goblin_rocket
  • 357
  • 2
  • 8
  • 17
  • 5
    As for the `None`, that's because your `get_rate` method doesn't return anything. See [How is returning the output of a function different from printing it?](//stackoverflow.com/q/750136). As for the `**kwargs`: You put them in the wrong place. They should be used in the `Hourly.__init__` method. And `Employee` shouldn't be calling `super().__init__(**kwargs)` or `super().__init__()` or anything of the sort. – Aran-Fey May 27 '18 at 07:59
  • OK thanks, so for the second one I should just remove **kwargs from the Employee __init__ as well as the super and it should work? I did that but Im still getting the same error. – goblin_rocket May 27 '18 at 08:12
  • 1
    Remove them from `Employee` and add them to `Hourly`: `def __init__(self, rate='', **kwargs):` – Aran-Fey May 27 '18 at 08:13
  • Like this https://pastebin.com/xsrEBDpx ? That still returns the same error. Also please see my post edits above, thanks. – goblin_rocket May 27 '18 at 08:17
  • 1
    Ah, it's because you're passing them as positional arguments, not keyword arguments. You can change all your `**kwargs` to `*args, **kwargs`. But notice that you're passing `'Bob'` as the value for the `rate` parameter, so maybe you should just use keyword arguments to avoid making mistakes like that. – Aran-Fey May 27 '18 at 08:20
  • oK, I *sort of* understand now, but what are those examples from the book doing differently with **kwargs? – goblin_rocket May 27 '18 at 08:31
  • Not sure what you mean. I don't think they're doing anything differently. – Aran-Fey May 27 '18 at 08:35
  • 1
    Ah, I think I understand what you're talking about. The classes in the book are designed for multiple inheritance. (You can see that `Friend` is inheriting from both `Contact` and `AddressHolder`.) You may find my answer [here](https://stackoverflow.com/questions/9575409/calling-parent-class-init-with-multiple-inheritance-whats-the-right-way/50465583#50465583) useful. – Aran-Fey May 27 '18 at 08:39
  • ok thank you, I will read that answer, still dont fully understand all this but getting there slowly! Feel free to answer and I will accept it :) – goblin_rocket May 27 '18 at 08:41
  • 2
    I'm not sure if I should. There are so many separate small problems in your code that it's probably a little too broad to be a good fit for StackOverflow. – Aran-Fey May 27 '18 at 08:44

3 Answers3

7

The poblem you have here isn't really specific to super but more specific to kwargs. If we throw most of your code away and remove the super it looks like this:

class Hourly(Employee):

    def __init__(self, rate=''):
        self.rate = rate
        some_crazy_function(**kwargs)

hourlystaff1 = Hourly('Bob', 'Foo', '23', 'M', '12/1/1980', '$15', '30')

There are two obvious problems: The __init__ function is getting more arguments passed than expected and in the body of the __init__ function is a reference to kwargs which is not defined anywhere. While here understanding **kwargs (and its sibling *args) is enough to fix the problem here super and **kwargs are very useful together. Lets first look why super is useful. Lets imagine we write some wrappers around subprocesses with some nice helper methods (the architecture is maybe not the best fit for the problem, but only ever seeing animals with inheritance is also not super helpful. Multiple inheritance is a really rare case, so it's hard to come up with good examples that are not Animals, GameEntities or GUIwidgets):

class Process:
    def __init__(self, exe):
        self.exe = exe
        self.run()

class DownloadExecutableBeforeProcess(Process):
    def __init__(self, exe):
        self.download_exe(exe)
        Process.__init__(self, exe)

Here we are doing inheritance and we do not even need to use super - we can just use the name of the superclass explicitly and have the behavior we want. We could rewrite to use super here but it would not change the behavior. If you only inherit from one class you don't strictly need super, although it can help you to not repeat the classname you inherit from. Lets add to our class hirarchy and include inherting from more than one class:

class AuthenticationCheckerProcess(Process):
    def __init__(self, exe, use_sha=True):
        self.check_if_authorized(exe, use_sha)
        Process.__init__(self, exe)

class DownloadAndCheck(DownloadExecutableBefore, AuthenticationCheckerProcess):
    def __init__(self, exe):
        DownloadExecutableBefore.__init__(exe)
        AuthenticationCheckerProcess.__init__(exe, use_sha=False)

If we follow the init of DownloadAndCheck we see that Process.__init__ is called twice, once through DownloadExecutableBefore.__init__ and once through AuthenticationCheckerProcess.__init__! So our process we want to wrap is also run twice, which is not what we want. Here in this example we could fix this easily by not calling self.run() in the init of process, but in realworld cases this is not always so easy to fix like here. Calling Process.__init__ just seems wrong in this case. Can we somehow fix this?

class DownloadAndCheck(DownloadExecutableBefore, AuthenticationCheckerProcess):
    def __init__(self, exe):
        super().__init__(exe, use_sha=False)
        # also replace the Process.__init__ cals in the other classes with super

super fixes this problems and will only call Process.__init__ once. It will also take care of the order in which the function should run, but this is not a big problem here. We still have a problem: use_sha=False will get passed to all initializers, but only one actually needs it. We can't really only pass the variable to only the functions that need it (because figuring that out would be a nightmare) but we can teach the other __init__s to just ignore the keywoard:

class Process:
    def __init__(self, exe, **kwargs):
        # accept arbitrary keywoards but ignore everything but exe
        # also put **kwargs in all other initializers
        self.exe = exe
        self.run()

class DownloadExecutableBeforeProcess(Process):
    def __init__(self, exe, **kwargs):
        self.download_exe(exe)
        # pass the keywoards into super so that other __init__s can use them
        Process.__init__(self, exe, **kwargs)

Now the super().__init__(exe, use_sha=False) call will succeed, each initializer only takes the keywoards it understands and simply passes the others further down.

So if you have mutliple inheritance and use different (keywoard) arguments super and kwargs can solve your problem. But super and multiple inheritance is complicated, especially if you have more inheritance layers than here. Sometimes the order in which functions should be calles is not even defined (and python should throw an error then, see e.g. explenation of change of MRO algorithm). Mixins might even require a super().__init__() call although they don't even inherit from any class. All in all you gain a lot of complexity in your code if you use multiple inheritance, so if you don't really need it, it's often better to think of other ways to model your problem.

syntonym
  • 7,134
  • 2
  • 32
  • 45
  • 1
    Good answer. It might be more suitable for [this question](https://stackoverflow.com/q/9575409/1222951) though. I don't think it'll do much good here, considering how messy the question is. Do consider moving your answer to the question I linked; it'll be seen by far more people there (and you only have to make some minor changes). – Aran-Fey May 27 '18 at 14:21
  • My one complaint about this question is related to this statement of yours: *"If you only inherit from one class super you do not need super"*. That is definitely true, but it's still good practice to use `super`, if only because it lets you avoid having to hard-code the parent class's name. I fear that the way you phrased that sentence might discourage people from using `super` with no good reason. – Aran-Fey May 27 '18 at 14:25
  • @Aran-Fey Thanks. To me it feels like understanding the question in the thread you linked already needs a basic understanding of `super`, which I tried to convey in my answer. I didn't really found an SO question that asks for basic knowledge provided in my answer, although [the linked blog entry](https://rhettinger.wordpress.com/2011/05/26/super-considered-super/) is probably the best ressource on `super` I found to date. I changed the phrasing slightly to incorporate your complaint, although personally I prefer to avoid `super` in cases it's not needed. – syntonym May 27 '18 at 14:56
  • thanks, great answer and I definitely need to read/practise more with this – goblin_rocket May 28 '18 at 02:27
  • 1
    @goblin_rocket If you have not seen it already [this blog article](https://rhettinger.wordpress.com/2011/05/26/super-considered-super/) also linked in the answer Aran-Fey linked to is really good. The only think missing is mixins, where you need to call super although it doesn't have a super class. – syntonym May 28 '18 at 14:04
0

**kwargs in the simples words is a dict

class Employee:
    def __init__(self, firstname='', lastname='', age='', dob='', **kwargs):
        super().__init__(**kwargs)

the **kwargs in the constructor __init__, means you don't know (not in the meaning of not knowing but sort of ignoring) or you care less about the arguments in the parent class Employee. So, how python knows which keywords to accept. The answer here super().__init__(**kwargs) this **kwargs specifically means whatever keyword in the Employee class will be accepted. Here's the logic, the kwargs in __init__() must be there which is actually in the parent class, therefore, kwargs must be is the super().__init__() as well.

Now, let us move to this line of code:

class Hourly(Employee):

    def __init__(self, rate=''):
        self.rate = rate
        super().__init__(**kwargs)

the __init__ does not contain **kwargs, so how can super().__init__ know what parameters to inherit. We need to fix that by adding **kwargs, so the code will be like this:

class Hourly(Employee):

    def __init__(self, rate='', **kwargs):
        self.rate = rate
        super().__init__(**kwargs)

Let us turn to the arguments now: How many args are there in the __init__() method?

--> We have two positional arguments (self the instance itself. the second arg is rate which it takes a default value. --> And the third arg is kwargs, so you must pass a dictionary here. That is the reason I started saying kwargs is a dict:

Let us analyze the error:

 File "staff_b.py", line 33, in <module>
    bob = Hourly('Bob', 'Bar', '23', '12/1/2019')
TypeError: __init__() takes from 1 to 2 positional arguments but 5 were given

The constructor __init__() takes one or maximum two (why 1 or two, because one argument has a default value (rate = ' ') Not reasonable default arg by the way. But we provided five.

So, Let us find a way to add only one or two (the instance itself, or the instance plus rate when you want to overwrite the default value) and adding the key-word arg. We can do that in two ways:

first: pass a dict when instantiating, but you have to put ** at the beginning: Be sure to pass the keys exactly as written in the parent class.

bob = Hourly(rate='12%', **{'firstname':'Bar', 
                                            'lastname':'Bar', 'age':23, 'dob': '12/1/2019'})

Second (the neat one): define a dict_variable then pass it when instantiating with ** upfront.

personal_info = {'firstname':'Bar', 'lastname':'Bar', 'age':23, 'dob': '12/1/2019'}
# then pass it when like this
bob = Hourly(rate='12%', **personal_info)

Finally: The __str__() has sex attribute which does not exist any class. It is either to add that in the parent class. I think this is obvious, and I am not going to talk about it. (Note, I excluded the __str__() from the full code, but you should do it in yours)

Here is the full code:

class Employee:
    def __init__(self, firstname='', lastname='', age='', dob=''):
        super().__init__(**kwargs)
        self.firstname = firstname
        self.lastname = lastname
        self.age = age 
        self.dob = dob 

class Hourly(Employee):
    def __init__(self, rate='', **kwargs):
        self.rate = rate
        super().__init__(**kwargs) 

    def get_rate(self):
        print('The hourly rate of {} is {}: '.format(self.firstname, self.rate))
        
personal_info = {'firstname':'Bar', 'lastname':'Bar', 'age':23, 'dob': '12/1/2019'}

bob = Hourly(rate='12%', **personal_info)

bob.get_rate()

Here is the result

The hourly rate of Bob is: 12% 

You can check the attributes as well this way:

for key, val in bob.__dict__.items():
    print(f'{key}: {val}')

to get the results

rate: 12%
firstname: Bob
lastname: Bar
age: 23
dob: 12/1/2019
Nimantha
  • 6,405
  • 6
  • 28
  • 69
Dr.saad
  • 1
  • 1
-2

This should work

class Employee:
    def __init__(self, firstname='', lastname='', age='', dob=''):
        self.firstname = firstname
        self.lastname = lastname
        self.age = age 
        self.dob = dob 

Then you have child class

class Hourly2(Employee):
    def __init__(self,*args, **kwargs):
        super(Hourly2,self).__init__(*args, **kwargs)

    def get_rate(self,rate=None):
    self.data=rate
    print ('My hourly rate is {}'.format(self.data))

Now,let us make instance

bob = Hourly2('Bob', 'Bar', '23', '12/1/2019')

We can check attributes

dir(bob)
['__class__',
 '__delattr__',
 '__dict__',
 '__dir__',
 '__doc__',
 '__eq__',
 '__format__',
 '__ge__',
 '__getattribute__',
 '__gt__',
 '__hash__',
 '__init__',
 '__init_subclass__',
 '__le__',
 '__lt__',
 '__module__',
 '__ne__',
 '__new__',
 '__reduce__',
 '__reduce_ex__',
 '__repr__',
 '__setattr__',
 '__sizeof__',
 '__str__',
 '__subclasshook__',
 '__weakref__',
 'age',
 'dob',
 'firstname',
 'get_rate',
 'lastname']

Finally

bob.age
'23'

And

bob.get_rate('$12')
My hourly rate is $12
Richard Rublev
  • 7,718
  • 16
  • 77
  • 121
  • 1
    That child class is completely pointless. Why have a method that takes an argument and returns it? And if you're going to assign that argument to an attribute, why is it called "**get**_rate"? And anyway, why not have the `rate` as a constructor parameter? – Aran-Fey May 27 '18 at 09:01
  • 1
    Yes, but it did something completely different than yours. – Aran-Fey May 27 '18 at 09:03
  • I have made edit,can you give example or link regarding constructor parameter. – Richard Rublev May 27 '18 at 09:30
  • this does not address how to work with warts and super, what if I do not want to specify explicitly the parameters in the __init__() of super? – Rodrigo Rivera Sep 12 '18 at 17:02