Arguments#
I have found it frustrating to visit online API sites to discover documentation that consists of something like the following:
my_method(*args, **kwargs)
This is especially annoying when there is no mention of what args
or kwargs
can be. We can’t solve
that problem, but we can at least understand what *args
, **kwargs
means and how to use them.
In short:
*args
means that the method can take any number of positional arguments.**kwargs
means that the method can take any number of named arguments.
Let’s define a couple of terms.
Term |
Definition |
---|---|
Positional Argument |
Required method arguments that must appear in a specific order |
Named Argument |
Optional method arguments that are given a default value in the prototype (method declaration). They will appear as |
Start with Print#
Let’s first explore the method print
to see how it works. Note that you can get help on print
by
using the following inside of Jupyter: help(print)
. Here is the help printed:
Help on built-in function print in module builtins:
print(...)
print(value, ..., sep=' ', end='\n', file=sys.stdout, flush=False)
Prints the values to a stream, or to sys.stdout by default.
Optional keyword arguments:
file: a file-like object (stream); defaults to the current sys.stdout.
sep: string inserted between values, default a space.
end: string appended after the last value, default a newline.
flush: whether to forcibly flush the stream.
The ...
above should be replaced with *args
. It should look like:
print(*args, sep=' ', end='\n', file=sys.stdout, flush=False)
In short, print
accepts as many position arguments as you want and it has four named arguments.
You will likely want to set end=''
at times to behave like Java’s System.out.print
(no
new line printed). And, if you want to print a list of values separated with commas, you could easily
override sep=', '
.
Packing & Unpacking Arguments#
When someone calls a method using *args
in the arguments (e.g. foo(*args)
) it unpacks the Tuple
and calls the method with each element in the Tuple as an argument (e.g. foo(1,2,3)
).
The same things happens when a method is called with **kwargs
syntax, except that the names are
also unpacked. In this case, kwargs is a dictionary
.
Here is some code to illustrate:
args = (1, 2, 3)
foo(*args) # Same as below
foo(1, 2, 3) # Same as above
kwargs = { 'a':1, 'b':'yes!', 'c':3 }
bar(**kwargs) # same as below
bar(a=1, b='yes!', c=3) # same as above
What this means is that you can technically define every method as follows:
def foo(*args, **kwargs):
# args behaves like a tuple. Just dereference elements.
# kwargs behaves like a dictionary.
# note: perhaps len(args) == 0. Your code should check!
if len(args) > 0:
print(args[0])
if 'name' in kwargs:
print(kwargs['name'])
# repack and pass along all named arguments to foo()
foo(**kwargs)
Examples#
Let’s say that we want to have a method with 7 arguments where 3 of them have default values. We’d define this method as shown below. Note that the three named arguments have default values.
# Example prototype with 4 positional arguments and 3 named arguments
def example_a(value1, value2, value3, value4, named1=1, named2=2, named3=3):
'''
value1, value2, value3 & value4 must be present when calling method()
named1, named2 & named3 are optional because they have default values.
'''
print(value1, value2, value3, value4, named1, named2, named3)
We can call example_a
in the following ways:
# we can call example_a in the following ways
example_a(3, 1, 4, 1) # output: 3 1 4 1 1 2 3
example_a('a', 'b', 'c', 'd', named2='foo') # output: a b c d 1 foo 3
example_a(0, 0, 'a', 'b', named3='bar', named1='foo') # output: 0 0 a b foo 2 bar
In every call, values for every positional argument is required.
In the first call, all the named arguments retain their default values: 1, 2, 3.
In the second call, we override the value for named2
only.
In the third call, we override named1
and named2
only.
Let’s get a little more complicated. In the following code, we are creating a method that allows for an arbitrary number of position arguments, zero or 50! The method has exactly two named arguments.
# example prototype with 1 required positional argument, any number of
# optional positional arguments, and two optional named arguments
def example_b(value1, *args, named1=1, named2=2):
'''
value1 is a required, positional argument
args is basically a Tuple that represents any number of un-named argments that appear
before all named arguments
named1 & named2 are optional named arguments that are given default values
'''
# when we put the '*' in front of args, we unpack the positional arguments
print(value1 + 10, *args, named1, named2)
The only line of code calls print
with 3 or more arguments. The first argument
to print
is value1 + 10
. It then will pass in zero or more arguments that were
passed into example_b
.
Let’s examine a few examples:
# we can call example_b in the following ways:
example_b(5, 10, 15, 20) # output: 15 10 15 20 1 2
example_b(0, named2='override') # output: 10 1 override
In the first call, we pass in 4 positional arguments. value1
will have the value 5.
Then, *args
will accept the next 3 arguments. args
is actually a Tuple
. In order
to pass all these values into print
as separate arguments, we have to unpack
the tuple
by using the *
.
In the second call, we pass in only 1 positional argument and then override only one named argument.
One more example:
def example_c(value1, *args, named1=1, named2=2, **kwargs):
'''
value1 is a required, positional argument
args is basically a Tuple that represents any number of un-named argments that appear
before all named arguments
named1 & named2 are optional, named arguments that are given default values
kwargs is basically a dictionary that represents any number of named argument
that appear after all the positional arguments
'''
# unpack args to all positional arguments
# unpack **kwargs to be all named arguments
print(value1 + 20, named1, named2, *args, **kwargs)
example_c(0, named1=10) # output: 20 10 2
example_c(2, 'foo', named2=0, sep='-') # output: 22-1-0-foo
named_args = { 'named1':2, 'named2':1, 'sep':':', 'end':'end_of_line' }
example_c(4, 3, **named_args) # output: 24:2:1:3end_of_line
First call
In the first call, value1=0
and named1=10
.
Second call
In the second call, value1=2
and 'foo'
is put into the *args
argument. named1
retains its default
value of 1
and named2
is overridden to have the value 0
. The named argument sep
is not found in
the example_c
prototype–not as a named argument–so it gets put into the **kwargs
argument and is subsequently unpacked and passed along to print. The API
print
has a named argument 'sep'
which is printed between each argument. This is why each value is
separated by '-'
. In summary, the code effectly calls print like this:
# print(value1+20, named1, named2, *args, **kwargs)
print( 2+20, 1, 0, 'foo', sep='-')
Third call
In the third call things get even more complicated. here we create a dictionary
that is a map of
named arguments with values. It contains four named arguments, two of which are explicitly named
in the example_c
prototype, and other two are defined in the print
prototype. So, the call results in:
value1 = 4
*args = (3, )
named1 = 2
named2 = 1
**kwargs = { 'sep':':', 'end':'end_of_line' }
# with the above values, the actual print statement is equivalent to:
print(4+20, 2, 1, 3, sep=':', end='end_of_line')