Argument Lists and Decorators

Variable-Length Arguments

One way to create a function that can accept different numbers of arguments is to have default arguments for some of our parameters.

In [1]:
def add(x, y=0, z=0):
    return x + y + z
In [2]:
# with 2 args
add(3,4)
Out[2]:
7
In [3]:
# with 3 args
add(3,4,5)
Out[3]:
12

This approach becomes cumbersome if there is a need to sometimes support a large number of arguments. Furthermore, once we've defined the function, there will be a maximum number of arguments we can ever pass.

Python supports another feature without these limitations, called variable-length arguments. It uses the * operator for this feature (as well as for multiplication).

It can be used two ways:

  1. convert a series of arguments passed in to a tuple (when defining a function)
  2. convert a sequence (tuple, list, etc) to a series of args (when calling a function)

Let's see (1) first:

In [4]:
def add(*nums):
    print(nums)
    print(type(nums))
    total = 0
    for x in nums:
        total += x
    return total
In [5]:
add()
()
<class 'tuple'>
Out[5]:
0
In [6]:
add(1, 2)
(1, 2)
<class 'tuple'>
Out[6]:
3
In [7]:
add(1, 2, 3, 4, 5)
(1, 2, 3, 4, 5)
<class 'tuple'>
Out[7]:
15

As you can see, we're calling the function like normal, but instead of having parameters inside the parentheses of def add(????):, we have *nums there. The star means that nums will be a tuple, containing whatever arguments get passed in, regardless of how many there are.

Let's see use case (2) now, which is the reverse of the former example (now we're going from a sequence to a bunch of arguments):

In [8]:
print(1, 2, 3, sep=",") # regular argument passing
1,2,3
In [9]:
nums = [1, 2, 3]
print(*nums, sep=",") # convert the nums list to a series of arguments
1,2,3

Of course, this is especially useful we want to pass a large number arguments.

In [10]:
nums = list(range(100))
print(*nums, sep=",")
0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99

Decorators

Sometimes it's useful to modify many of our functions in the same way. For example, maybe we have several functions for doing arithmatic, and we want them all to print the incoming arguments (a weird example, but maybe for debugging).

We could copy/paste the print statements to each, but copy/pasting often has its disadvantages (for example, if later we want to write to a file instead of printing, there might be dozens of functions we need to go back and change).

Decorators solve this problem. A decorator is a functions (1) that can replace regular functions (2) with yet other functions that have additional features (3). This is strange, and one of the more challenging things to conceptualize in CS 320. What makes it weirder still:

  • the def statement for the functions in part (3) are typically indented inside function (1)
  • every time function (1) is called, it may define a new function (3); over time, many functions might get defined with the same name
  • the decorator function (1) takes as an argument a reference to a function (2), and returns a reference to a function (3)

Let's try an example:

In [11]:
def print_decorator(fn_before):
    def fn_after(*args):
        print("DEBUG", args)
        return "TODO"
    return fn_after

@print_decorator
def add(x, y):
    print("I am adding!!!")
    return x+y

@print_decorator
def sub(x, y):
    return x-y

@print_decorator
def mult(x, y):
    return x*y

@print_decorator
def div(x, y):
    return x-y
In [12]:
result1 = add(3, 4)
DEBUG (3, 4)
In [13]:
result2 = sub(1, 2)
DEBUG (1, 2)
In [14]:
result3 = mult(8, 10)
DEBUG (8, 10)
In [15]:
print(result1)
print(result2)
print(result3)
TODO
TODO
TODO

What's happening here? When @print_decorator appears before, say, the add function, the following happens:

  1. print_decorator automatically gets called
  2. a reference to the original new add function is passed to the fn_before paramater. Note that we aren't using fn_before yet in this example
  3. print_decorator defines a new function, fn_after, which it it returns (note that this function takes a variable number of arguments -- this is common, but not required)
  4. add is redefined to be the same as fn_after, instead of the code the programmer originally wrote after the def statement

That redefinition is why calling add(3, 4) results in the debug print, and a return value of "TODO", but we never see the "I am adding!!!" message printed.

We can also see this if we look at add (without calling it!); it will say "print_decorator":

In [16]:
add
Out[16]:
<function __main__.print_decorator.<locals>.fn_after(*args)>

With most realistic use cases for decorators, we want to add some functionality to an existing function, without fulling replacing it. This is where fn_before comes in. fn_after can perform whatever extra functionality is is supposed to, but it will usually then call fn_before so that the original logic runs too.

Let's modify the earlier example to do that instead of returning "TODO" in fn_after:

In [17]:
def print_decorator(fn_before):
    def fn_after(*args):
        print("DEBUG", args)
        return fn_before(*args)
    return fn_after

@print_decorator
def add(x, y):
    print("I am adding!!!")
    return x+y

@print_decorator
def sub(x, y):
    return x-y

@print_decorator
def mult(x, y):
    return x*y

@print_decorator
def div(x, y):
    return x-y
In [18]:
add(3, 4)
DEBUG (3, 4)
I am adding!!!
Out[18]:
7

Great! Now we get the original functionionality of add (printing "I am adding!!!" and returning the sum), while getting the bonus functionality of the debug print.

In general, it will just feel like we're using an enhanced version of the add function, but we can always look at the functions (again, without calling them) to see the swap-out that happened:

In [19]:
add
Out[19]:
<function __main__.print_decorator.<locals>.fn_after(*args)>
In [20]:
sub
Out[20]:
<function __main__.print_decorator.<locals>.fn_after(*args)>

One last detail: those two functions above are different, even though they have to have the same name(fn_after). So we'll still get different results from calling add vs. sub.

In [21]:
add(3, 4)
DEBUG (3, 4)
I am adding!!!
Out[21]:
7
In [22]:
sub(3, 4)
DEBUG (3, 4)
Out[22]:
-1
In [ ]: