Functions#

Author: Mike Wood

Learning objectives: By the end of this notebook, you should be able to:

  1. Define a Python function with one or more arguments that returns one or more objects.

  2. Differentiate between local and global variables and when they can be modified

  3. Implement the lambda keyword to create short in-line functions

Defining Functions#

Functions in Python start with the keyword def and follow a similar syntax to other code blocks we have seen in Python so far - they use the typical colon and indentation formatting.

# write a simple function to describe the weather
def weather_report():
    print('It\'s hot!')

From the above code block, we can observe 3 things:

  • Functions do not need to be defined with input arguments

  • Functions do not need to have to return objects

  • Functions are not run when they are defined - they must be called to be run

# call the weather report function
weather_report()
It's hot!

There is a lot of flexibility in how function in Python can be defined. For example, we may choose to define input arguments and implement code blocks within our function:

# update the weather report function to include an input argument for temperature
def weather_report(temperature):
    if temperature>75:
        print('The temperature is '+str(temperature)+' F - it\'s hot!')
    else:
        print('The temperature is '+str(temperature)+' F - it\'s not hot.')

# call the function to view its output
weather_report(81)

# call the function again by explicitly calling the input parameter
weather_report(temperature = 70)
The temperature is 81 F - it's hot!
The temperature is 70 F - it's not hot.

We can also define optional arguments that are given a default value. These default values may be changed by the function user, if desired.

# update the weather report function to include an optional input argument
# to convert the temperature to celsius
def weather_report(temperature, convert_to_celsius=False):

    # add a code block to allow for conversion into celsius
    units = 'F'
    hot_temperature = 75
    if convert_to_celsius:
        temperature = (5/9)*(temperature-32)
        units = 'C'
        hot_temperature = (5/9)*(hot_temperature-32)
    
    if temperature>hot_temperature:
        print('The temperature is '+'{:.2f}'.format(temperature)+
              ' '+units+' - it\'s hot!')
    else:
        print('The temperature is ''{:.2f}'.format(temperature)+
              ' '+units+' - it\'s not hot.')

# call the function to view its output
weather_report(81)

# call the function again, this time converting to celsius
weather_report(81, convert_to_celsius=True)
The temperature is 81.00 F - it's hot!
The temperature is 27.22 C - it's hot!

All functions in Python have an output object. If its not defined, then the value is None. For example:

# assign the output of the weather report function to a variable
output_test = weather_report(81)

# print the variable
print(output_test)
The temperature is 81.00 F - it's hot!
None

Often, we would like our functions to return a given statement or other output rather than just printing something to the screen. In this case, we can use the return keyword.

# update the weather report function to output the statement
# instead of printing it
def weather_report(temperature, convert_to_celsius=False):

    # add a code block to allow for conversion into celsius
    units = 'F'
    hot_temperature = 75
    if convert_to_celsius:
        temperature = (5/9)*(temperature-32)
        units = 'C'
        hot_temperature = (5/9)*(hot_temperature-32)
    
    if temperature>hot_temperature:
        output_statement = 'The temperature is '+str(temperature)+' '+units+' - its hot!'
    else:
        output_statement = 'The temperature is '+str(temperature)+' '+units+' - its not hot.'

    return(output_statement)

# store the output of weather_report into a variable called report
report = weather_report(81, convert_to_celsius=False)

When defined with an output, we can print the statement when desired:

# now print the report variable
print(report)
The temperature is 81 F - its hot!

🤔 Mini-Exercise#

Goal: Define a function called convert_miles to convert miles to kilometers. Provide an optional keyword that allows the user to convert to meters instead of kilometers. There are 6.2 miles in 10 kilometers and 1000 meters in 1 kilometer.

đź’ˇ Solution#

# define the convert_miles function
def convert_miles(miles, convert_to_meters = False):
    output = miles*10/6.2

    if convert_to_meters:
        output = output*1000

    return(output)

# test your function with 3.1 miles (should return 5)
print(convert_miles(3.1))

# test your function with 9.3 miles and the optional parameter to return meters (should return 15000)
print(convert_miles(9.3, convert_to_meters = True))
5.0
15000.0

Functions with an indefinite number of arguments#

It is often handy to define functions that have an open-ended number of arguments. There are two primary ways that this type of feature can be implemented.

First, a function can allow for an open number of arguments to be read in as a tuple. To store the arguments as a tuple, use the single astericks syntax:

# define a function that calculates the average of an indefinite set of numbers
def average(*numbers):

    # print the type of numbers
    print(type(numbers))

    # write code to compute the average
    sum = 0
    counter = 0
    for number in numbers:
        sum += number
        counter += 1
    return(sum/counter)

# test your function with a set of numbers
print(average(1,2,3))
<class 'tuple'>
2.0

Alternatively, a function can be used to take in a set of arguments and store them in a dictionary. The syntax is similar except that two astericks symbols are used:

# define a function that will calculate the molecular
# mass of one mole of a given molecule the function
# should take in a dictionary of the number of each 
# atom in the molecule
# for example, for molecule propane (C3H8), the
# function would be called as molecule_mass(C=3, H=8)
# which would be read into the dictionary {'C':3, 'H':8}
def molecule_mass(**atoms):

    # define some atomic masses in a dictionary
    # only showing a few examples for now
    masses = {'H':1.008,
               'C':12.01,
               'O':15.99}
    
    # print atoms and the type
    print(atoms)
    print(type(atoms))

    # add the masses of each atom to the total mass
    total_mass = 0
    for atom in ['H','C','O']:
        if atom in atoms:
            total_mass += masses[atom]*atoms[atom]

    return(total_mass)

# calculate the mass of carbon dioxide (CO2)
print(molecule_mass(C=1, O=2))
{'C': 1, 'O': 2}
<class 'dict'>
43.99

Local vs Global Variables#

Local variables are those which are defined inside of a function while global variables are those stored outside of a function. But when does the global variable space know about the local variables, and vice versa? Even more importantly, when can a function modify a global variable?

Local Variables#

# write a function to calculate the area of a circle given its radius
def circle_area(radius):
    pi = 3.1415
    area = pi*radius**2
    return(area)

# print the circle area
print(circle_area(5))

# print the variable pi - what happens?
# print(pi)
78.53750000000001

A local variable can be made accessible in the global namespace using the keyword global. Try this in the code above.

Global Variables#

The code on the interior of functions knows about global variables.

# define the number pi with 4 decimal places
pi = 3.1415

# write a function to calculate the area of a circle given its radius
def circle_area(radius):
    area = pi*radius**2
    return(area)

# print the circle area
print(circle_area(5))

# print the variable pi - what happens?
print(pi)
78.53750000000001
3.1415

However, a function cannot modify a global variable - try to change the value of pi inside the function above. What happens?

# define a string for department and a list of strings for classes
department = 'CS'
classes = ['122','123','146']

# print the deparment and classes
print(department, classes)

# define a function to change the department and classes
def change_schedule(dept, class_list):
    dept = 'Math'
    class_list.pop(-1)
    class_list.append('101')

# call the function
change_schedule(department, classes)

# print the deparment and classes again - how do they differ?
print(department, classes)
CS ['122', '123', '146']
CS ['122', '123', '101']

âť— Caution! Be careful about the naming of variables inside and outside your functions. If you do not intend for your function to change your global variables, be sure to make copies or otherwise ensure you are not operating on the global object.

Doc(umentation) Strings#

It is good practice to document your functions with “doc strings”. Doc strings are encapsulated with triple quotes at the top of the function body. When formatted correctly the help method can print the description. In addition, the doc strings can be used to automatically format a ReadTheDocs page when code is published online.

# write a function to calculate the area of a circle given its radius
# add a doc string note to the function
def circle_area(radius):
    """
    Function to calculate the area of a circle given its radius

    :param radius: this radius of the circle
    :returns: the area of the circle
    """
    area = pi*radius**2
    return(area)
help(circle_area)
Help on function circle_area in module __main__:

circle_area(radius)
    Function to calculate the area of a circle given its radius
    
    :param radius: this radius of the circle
    :returns: the area of the circle

Lambda Functions#

The main way to define a function in Python uses the def keyword as shown above.

# define a function called sqrt that returns the square root of x
def sqrt(x):
    return x**0.5

# use the function to print the square root of 9
print(sqrt(9))
3.0

However, if the function is a 1-line function designed to be used on the fly, it can be declared as a lambda function. We can see the syntax in the following piece of code:

# define a function called sqrt_lambda that defines the square root function
# using the lambda notation
sqrt_lambda = lambda x : x**0.5

# use the function to print the square root of 9
print(sqrt_lambda(9))
3.0

Lambda functions are get their name from “Lambda Calculus” - a system of mathematical logic based on function abstraction.

lambda Example: Indices of a sorted list#

One way that lambda functions are useful is when providing a sorting key. First, explore the sorted function with an intuitive example

# define a list with 5 numbers
my_list = [8,-3,-7,1,2]

# sort the list with the values given
print(sorted(my_list))

# define a function to calculate the absolute value
def abs_val(number):
    if number<0:
        number*=-1
    return(number)

# sort the list passing the key for the absolute value
print(sorted(my_list, key = abs_val))
[-7, -3, 1, 2, 8]
[1, 2, -3, -7, 8]

A function can also be used to provide the indices of a sorted list.

# define a function called list_val to return
# the value of my_list at a given index
def list_val(index):
    return(my_list[index])

# define a list of the indices
indices = range(len(my_list))

# print the indices of a sorted list
print('Indices of sorted list:')
print(sorted(indices, key = list_val))

# print the list as a reminder
print('The list:')
print(my_list)
Indices of sorted list:
[2, 1, 3, 4, 0]
The list:
[8, -3, -7, 1, 2]

Alternatively, this function can be written with a lambda function as a one-liner as follows:

# calculate the indices with the lambda function
indices = sorted(range(len(my_list)), key=lambda k: my_list[k])
print(indices)
[2, 1, 3, 4, 0]

🤔 Mini-Exercise#

Goal: Use a lambda function inside Python’s map function to quickly calculate the sine of a list of numbers.

Python has a built-in function called map to quickly apply a function to a list of numbers. For example, consider the following code to calculate the square of a list of numbers:

# define a squared function
def squared(x):
    return x**2

# define a list of numbers
numbers = [1,2,3]

# calculate the square of the numbers
print(list(map(squared, numbers)))
[1, 4, 9]

In the following code block, calculate the sin of the numbers -\(\pi\)/2, 0, and \(\pi\)/2. The sine of a number can be estimated with the first four terms of its Taylor expansion as:

\[ sin(x) \approx x - \frac{x^3}{6} + \frac{x^5}{120} - \frac{x^7}{5040} \]

You can estimate \(\pi\) as 3.1415.

# create the list of values
values = [-3.1415/2, 0, 3.1415/2]

# calculate the sin of the values as described above
print(list(map(lambda x: x - x**3/6 + x**5/120 - x**7/5040, values)))
[-0.9998431417619411, 0.0, 0.9998431417619411]