def add(x, y=0, z=0): return x + y + z
# with 2 args add(3,4)
# with 3 args add(3,4,5)
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:
Let's see (1) first:
def add(*nums): print(nums) print(type(nums)) total = 0 for x in nums: total += x return total
() <class 'tuple'>
(1, 2) <class 'tuple'>
add(1, 2, 3, 4, 5)
(1, 2, 3, 4, 5) <class 'tuple'>
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):
print(1, 2, 3, sep=",") # regular argument passing
nums = [1, 2, 3] print(*nums, sep=",") # convert the nums list to a series of arguments
Of course, this is especially useful we want to pass a large number arguments.
nums = list(range(100)) print(*nums, sep=",")
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:
defstatement for the functions in part (3) are typically indented inside function (1)
Let's try an example:
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
result1 = add(3, 4)
DEBUG (3, 4)
result2 = sub(1, 2)
DEBUG (1, 2)
result3 = mult(8, 10)
DEBUG (8, 10)
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:
print_decoratorautomatically gets called
addfunction is passed to the
fn_beforeparamater. Note that we aren't using
fn_beforeyet in this example
print_decoratordefines 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)
addis redefined to be the same as
fn_after, instead of the code the programmer originally wrote after the
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":
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
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
DEBUG (3, 4) I am adding!!!
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:
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
DEBUG (3, 4) I am adding!!!
DEBUG (3, 4)