I am trying to understand the use of *args
and **kwds
when creating subclasses in Python.
I want to understand why this code behaves the way it does. If I leave out the *args
and **kwds
in a call to super().__init__
, I get some strange argument unpacking.
Here is my test case:
class Animal(object):
def __init__(self, moves, num_legs):
self.moves = moves
self.num_legs = num_legs
def describe(self):
print "Moves :{} , num_legs : {}".format(self.moves, self.num_legs)
class Snake(Animal):
def __init__(self, poisonous, *args, **kwds):
self.poisonous = poisonous
print "I am poisonous:{}".format(self.poisonous)
# This next line is key. You have to use *args , **kwds.
# But here I have deliberately used the incorrect form,
# `args` and `kwds`, and am suprised at what it does.
super(Snake, self).__init__(args, kwds)
Now, when I create instances of the Snake subclass, which contains the erroneous call to super(…).__init__
(where I use args
and kwds
instead of *args
and **kwds
), I get some interesting “argument unpacking”.
s1 = Snake(False, moves=True, num_legs=0)
s2 = Snake(poisonous=False, moves=True, num_legs=1)
s3 = Snake(False, True, 3)
s1.describe()
s2.describe()
s3.describe()
What I get is:
Moves :() , num_legs : {'moves': True, 'num_legs': 0}
Moves :() , num_legs : {'moves': True, 'num_legs': 1}
Moves :(True, 3) , num_legs : {}
So why is it that in s1
and s2
, __init__
assumes that moves = True
and num_legs = 0
or 1
are keyword arguments, and sets the num_legs
to a dict?
In s3
, it unpacks both of the variables to moves
(in class Animal
) as a tuple.
I stumbled into this as I was trying to understand argument unpacking. Sorry in advance—I don't know how to frame this question any better.
In Snake.__init__
, args
is a tuple of all positional arguments after poisonous
and kwds
is a dict of all the keyword arguments apart from poisonous
. By calling
super(Snake,self).__init__(args,kwds)
you assign args
to moves
and kwds
to num_legs
in Animal.__init__
. That’s exactly what you are seeing in your output.
The first two calls don’t have any positional arguments apart from poisonous
, so args
and consequential moves
is an empty tuple. The third call has no keyword arguments, so kwds
and consequential num_legs
is an empty dict.
In short: def __init__(self,poisonous,*args,**kwds):
means: capture positional arguments in a tuple args
and keyword arguments in a dictionary kwds
. Similarly, super(Snake,self).__init__(*args, **kwds)
means: unpack the tuple args
and the dictionary kwds
into arguments so that they're passed separately to __init__
.
If you don't use the *
and **
then you're passing args
and kwds
as they are, which means you're getting a tuple and a dictionary.
As you've said, you'd need to write:
super(Snake,self).__init__(*args, **kwds)
to properly pack / unpack the arguments. In your current code you're not packing / unpacking the arguments so it sets num_legs
to a dictionary as that's what kwds
is at the moment.
If you don't give the arguments names then they're positional arguments. Hence Snake(False,True,3)
are all positional arguments.
If you do give the arguments names then they're keyword arguments: Snake(poisonous=False,moves=True,num_legs=1)
.
In the first case you're combining both one positional argument and two keyword arguments: Snake(False,moves=True,num_legs=0)
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With