Advanced Function Topics

Advanced Parameter Topics

When we last talked about parameters, it was just the basics: for each value you want your function to have, you have one parameter. Python supports a lot of options when it comes to parameters though. You can:

Default Values

We'll start with the most basic of these topics: default values. The greet_user2 function from earlier allowed the user to pass in a greeting and a name. If we want, we could provide a default greeting. To do that, in the parameter list, after the parameter's name, we assign the default value. In the example below, we provide a default value for greeting.

Code Output
def greet_user3(name, greeting='Hello'):
    print(greeting, ", ", name, "!", sep="")

greet_user3("Michael", 'Hi')
greet_user3('Michael')
Hi, Michael!
Hello, Michael!

Since greeting has a default value, if the user doesn't provide an argument for greeting, the default is used. However, if the user provides an argument, then that argument is used.

If you compare greet_user3 with greet_user2, you'll notice that the order of the parameters is switched. That's because Python requires parameters with default values to come after parameters without default values. For example, this is the wrong form for a function header:

greet_userBAD(greeting='Hello', name)

The default parameter is evaluated once, so you want to be careful not to modify it accidentally. Here's a simple example of this problem:

Code Output
def append_value(value, array=[]):
    array.append(value)
    return array

a1 = append_value(5)
print('a1 = ', a1)

a2 = append_value(1)
print('a2 = ', a2)

a3 = append_value(3)
print('a3 = ', a3)

a4 = ['a', 'b', 'c']
a5 = append_value('d', a4)
print('a4 = ', a4)
print('a5 = ', a5)

a6 = append_value(6)
print('a6 = ', a6)
a1 =  [5]
a2 =  [5, 1]
a3 =  [5, 1, 3]
a4 =  ['a', 'b', 'c', 'd']
a5 =  ['a', 'b', 'c', 'd']
a6 =  [5, 1, 3, 6]

Notice that when no list is passed into append_value, it uses the default. The default starts out as an empty list, but each time something gets appended to it, the list holds onto that value it's still there the next time the function is called with no list passed in.

To fix this, use None as a default value. Then, check whether array is None. If it is, assign into it a new list. For example:

Code Output
def append_value2(value, array=None):
    if array == None:
        array = []
    array.append(value)
    return array

a1 = append_value2(5)
print('a1 = ', a1)

a2 = append_value2(1)
print('a2 = ', a2)

a3 = append_value2(3)
print('a3 = ', a3)

a4 = ['a', 'b', 'c']
a5 = append_value2('d', a4)
print('a4 = ', a4)
print('a5 = ', a5)

a6 = append_value2(6)
print('a6 = ', a6)
a1 =  [5]
a2 =  [1]
a3 =  [3]
a4 =  ['a', 'b', 'c', 'd']
a5 =  ['a', 'b', 'c', 'd']
a6 =  [6]

Keyword Arguments

Both value and array are actually keyword arguments. You can specify values for keyword arguments in a unique way. When calling a method, instead of just specifying the argument, you can explicitly state that the parameter is getting a certain value. For example:

append_value2(10, array=[12,3])

Why is this useful? In this case it's not. However, it can be useful if there are a lot of keyword parameters and you only want to give some of them non-default values. For example:

Code Output
def greet_user4(name, greeting='Hello', punct='!', convert_name=False):
    if convert_name == 'upper':
        name = name.upper()
    if convert_name == 'lower':
        name = name.lower()
    if convert_name == 'title':
        name = name.title()
    if convert_name in ('capitalize', 'cap'):
        name = name.capitalize()
    
    print(greeting, ", ", name, punct, sep="")

greet_user4("Michael", 'Hi')
greet_user4('Michael', punct='.')
greet_user4('Michael', convert_name='cap', greeting='Hi')
Hi, Michael!
Hello, Michael.
Hi, MICHAEL!

You can also use default values to make some parameters optional. In the example below, if the average function receives just one argument, then it assumes that it's a list or tuple and computes the average of the list/tuple. However, if it receives two values, then it assumes the first is the sum and the second is the number of values and computes the average from those values.

Code Output
def average(total, count=None):
    if count == None:
        if not isinstance(total, (list, tuple)):
            raise TypeError('If only one argument is given, it must be a list or tuple')
        return sum(total) / len(total)
    else:
        return total / count

average((1,2,3,4))
average(4, 10)
2.5
0.4

Var-Positional Arguments

You can also have an unlimited number of positional arguments (actually, it's limited to how much your computer's memory can hold). This is called var-positional for "variable-positional" (variable in the sense that the number of arguments can vary). To accomplish this, you need to create a special parameter to hold all of these values. This special parameter must start with an asterisk (*). You can have only one var-positional parameter.

Code Output
def average2(total, count=None, *values):
    if count == None:
        if not isinstance(total, (list, tuple)):
            raise TypeError('If only one argument is given, it must be a list or tuple')
        return sum(total) / len(total)
    elif len(values) == 0:
        return total / count
    else:
        return sum((total, count)+tuple(values)) / (2+len(values))

average2((1,2,3,4))
average2(4, 10)
average2(4, 10, 8)
2.5
0.4
7.333333333333333

Notice that if exactly two values are given, then *values will be empty.

If you want to specify both keyword arguments and positional arguments, the keyword arguments must come after the positional ones. Same for the parameters. The example below will not work because average2 has count come before *values. So when average2 is called in the example below, count will get two values, 2 and 8.

average2(7, 2, 3, 4, count=8)

To fix this problem, you must rearrange the parameter list to put *values before the keyword parameters. This also requires some changes to the body of the function and in the way you call the function:

Code Output
def average3(total, *values, count=None):
    if isinstance(total, (list, tuple)):
        return sum(total) / len(total)
    elif count != None:
        return total / count
    else:
        return sum((total, )+tuple(values)) / (1+len(values))

average3((1,2,3,4))
average3(4, count=10)
average3(4, 10, 8)
2.5
0.4
7.333333333333333

Mixing keyword parameters and a var-positional parameters can get confusing. In practice, you don't often mix the two (although we've been using one that does since the beginning of the semester: print.

Var-Keyword Arguments

Just like you can have a variable number of positional arguments, you can have a variable number of keyword arguments. In your parameter list, you create a special parameter to hold all of the specified keyword arguments. This special parameter must start with two asterisks (**). You can have only one var-keyword parameter. This special parameter will then hold all of the keyword arguments (that don't belong to keyword parameters) in a dictionary, with the keys being the parameter names and the values being the arguments.

You might use var-keyword arguments if your function can take a lot of optional arguments but in most cases the user will only want to specify some of them. For example, if one parameter is used to specify how the function will behave and each behavior has different parameters that are needed (see the example below). Another is if there are a lot of parameters, but the user will likely only change the defaults for one or two of them.

Code Output
def calculate_stats(values, **kwargs):
    '''
        Takes a list of numbers (values) and computes some statistics, then
        returns either a specially-formatted string (perhaps suitable for
        writing to a file), a tuple of values, or a dictionary.
        
        store is used to specify the storage (and return) type: 'dict',
        'list', or 'str' (default is list). If 'str' is chosen, then
        the additional options are:
         * delim: the delimiter to use to separate values (default=',')
         * endl: the end-of-line string to use (default='\\n')
         * incl_head: include a header line with the label for each column
           (True or False, default=False)
        
        as_type is used to convert the elements in the values collection
        into the specified type (default is float)
    '''
    
    if not isinstance(values, (list, tuple, set)):
        raise TypeError('values must be a list, tuple, or set')
    
    to_type = kwargs.get('as_type', float)
    values = [to_type(v) for v in values]
    
    retval = []
    labels = []
    
    length = len(values)
    retval.append(length)
    labels.append('length')
    
    total = sum(values)
    retval.append(total)
    labels.append('total')
    
    retval.append(float(total) / length)
    labels.append('mean')
    
    smallest = min(values)
    retval.append(smallest)
    labels.append('min')
    
    largest = max(values)
    retval.append(largest)
    labels.append('max')
    
    retval.append(largest - smallest)
    labels.append('range')
    
    store = kwargs.get('store', list)
    if store in (dict, 'dict'):
        return dict(zip(labels, retval))
    elif store in (str, 'str'):
        delim = kwargs.get('delim', ',')
        end = kwargs.get('endl', '\\n')
        output = ''
        if kwargs.get('incl_head', False):
            output += delim.join(labels) + end
        output += delim.join([str(v) for v in retval]) + end
        return output
    else:
        return labels, retval
    

array = [5.4, 3.5, 7.1, -2.4]
results1 = calculate_stats(array)
print('results1 =', results1)
print()

results2 = calculate_stats(array, store=dict)
print('results2 =', results2)
print()

results3 = calculate_stats(array, store=dict, as_type=int)
print('results3 =', results3)
print()

results4 = calculate_stats(array, store=str, incl_head=True)
print('results4 =', results4)
results1 = (['length', 'total', 'mean', 'min', 'max', 'range'], [4, 13.6, 3.4, -2.4, 7.1, 9.5])

results2 = {'mean': 3.4, 'length': 4, 'range': 9.5, 'max': 7.1, 'min': -2.4, 'total': 13.6}

results3 = {'mean': 3.25, 'length': 4, 'range': 9, 'max': 7, 'min': -2, 'total': 13}

results4 = length,total,mean,min,max,range\n4,13.6,3.4,-2.4,7.1,9.5\n

Notice that as_type is actually taking a function and applying that function to each element in values. You could actually pass in any function with one parameter. For example:

Code Output
def squared(val):
    return val * val

results5 = calculate_stats(array, as_type=squared)
print('results5 =', results5)
results5 = (['length', 'total', 'mean', 'min', 'max', 'range'], [4, 97.58, 24.395, 5.76, 50.41, 44.65])

Lambda Functions

Lambda functions are also called anonymous functions. They're functions without a name. They're typically used for small, quick operations. The format for a lambda function is:

lambda parameter_list: value_to_return

The parameter_list is, of course, the parameter list for your function. The value_to_return is, of course, the value your function will return.

Lambda functions differ from regular functions in a couple of ways:

For example, to rewrite the squared function from above:

squared = lambda val: val*val

It can then be used the same way as above:

results5 = calculate_stats(array, as_type=squared)
print('results5 =', results5)

How is this any better than a regular function? Its usefulness comes in when you just need a quick function for a brief time, like in our example above. Technically, we could just do:

results5 = calculate_stats(array, as_type=lambda val: val*val)
print('results5 =', results5)

Global Variables

Variables created in a function exist until that function is over, then they're destroyed. Variables created in a function only exist within that function. However, variables created outside of a function might be accessible inside the function. Variables created outside of functions (and classes) are called global variables. In the example below, val is a global variable.

Code Output
val = 5
def print_val():
    print(val)

print_val()
5

However, this only works for variables you are getting the value of. If you try to set the global variable from within a function, that will not change the global variable:

Code Output
val = 5
def change_val1():
    val = 10

print('val before =', val)
change_val1()
print('val after =', val)
val before = 5
val after = 5

If you try mixing getting a global variable's value and changing its value, this will also create a problem:

Code Output
val = 5
def change_val2():
    print('val inside =', val)
    val = 10

print('val before =', val)
change_val2()
print('val after =', val)
val before = 5
Traceback (most recent call last):
  File <stdin>, line 7, in <module>
    change_val2()
  File <stdin>, line 3, in change_val2
    print('val inside =', val)
UnboundLocalError: local variable 'val' referenced before assignment

If you would like to change a global variable within a function, you must tell the function that the variable is a global variable using the global keyword:

Code Output
val = 5
def change_val3():
    global val
    print('val inside =', val)
    val = 10

print('val before =', val)
change_val3()
print('val after =', val)
val before = 5
val inside = 5
val after = 10

You can also create a global variable from within a function:

Code Output
def get_data_directory():
    global data_dir
    data_dir = input("What folder is the data stored in? ")

get_data_directory()
print(data_dir)
What folder is the data stored in? 'C:\Users\mil28\Documents\'
'C:\Users\mil28\Documents\'

It is considered bad programming practice to modify global variables from within a function. Since functions are generally self-contained, the user would not expect variables outside of the function to change when a function is called. However, global constants are considered ok. This is why you can easily access global variables inside a function, as long as you don't change them.

<< Previous Notes Daily Schedule