Be sure to do all exercises and run all completed code cells.

If anything goes wrong, restart the kernel (in the menubar, select Kernel\(\rightarrow\)Restart).


Writing Functions

Remember to type in and run all the examples as you go through these materials.
Study each example and make sure you understand what is happening before moving on to the next

Functions

Another crucial part of programming is the ability to define functions when we want to do the same thing in several places.
A function is a rule that takes input values and produces output values.
Python functions are not quite the same as mathematical functions.
However in Python a function that is called several times with the same input can return different values each time. A Python function can also have side effects, such as output to the screen or to a file.

We will start of by exploring some of the functions that are available in Python and then we will see how to build our own functions.

Built-in Functions

We have already come across several functions that are available directly in Python: print, int, float, round, list, range
Some others are type, abs, min and max.

  • To use a function you need the name of the function followed by the arguments in brackets.

    • The arguments can be numbers, variables, calculations or other functions.

The functions we had already seen and also the abs and type function take one input, known as its “argument” and return one thing back, known as the result.

int(4.436e2), round(4.436e2), float(-68), type(42), type("Hello")
abs(5), abs(-5), abs(6.01 + 0.01), abs(-6.01 - 0.01)

The min and max functions take two (or more) arguments and return one result.

If there are several arguments they have to be separated by commas.

max(-3, 5)
min(-3, 5, 0, -10)

Returned values

Some functions like sqrt() return a value that can be used, others just perform a function but do not give anything back, such as the print() function, which only prints to the screen but does not return a value:

from math import sqrt
a = sqrt(2)
print(a)
print(42)
b = print(42)
print(b)
  • A special value of None was returned to the variable b.

Another very useful built-in function is the help function.
The help function takes a single argument and prints useful information about the thing passed on as the input argument.
We can use this get information about what a function does and how to use it.

help(abs)

Imported Functions

In the previous section we saw that to access certain objects (constants or functions), we have to load a library first.

import math

print(math.exp(1))
from math import sqrt, sin, pi

print(sqrt(36))
print(sin(pi/2))
x = 2
print(
    math.log(math.exp(x)), 
    math.exp(math.log(x))
)

The mathematical functions in the math module always work with floating point numbers. This means that integers will be converted to floating point numbers first. The expression math.sqrt(36) will return the floating point number 6.0 even though the result could be represented by an integer.

The help function can also be applied to modules. This will result in a list of all the functions and constants contained in that module, together with their documentation. This information can also be found in the Python documentation, but it is often convenient to have it available directly.

  • Try this for the math module by printing help(math).

The built-in function dir allows us to get just a list of names of functions and constants for a module.

import math

print(dir(math))

Defining Functions

We can define our own functions using the def keyword.

def sq(x):
    return x**2
  • A function definition starts with the keyword def,

  • then comes the name of the function and a set of brackets,

    • The names of functions and parameters have to follow the same rules as the names of variables
      (letters, numbers and underscores, no numbers or underscores at the start).

  • inside the brackets put any parameters that are to be used in the function

    • multiple parameters are separated by commas.

  • a colon must follow the brackets

  • After the colon, starting on the next line, we can have one or more lines of code that make up the function.

    • anything the function does must be indented by the same number of spaces (conventionally 4)

    • the contents of the function are known as a “code block” (see below).

    • Anything indented will be run together when the function is used, but nothing afterwards.

Once a function is defined, it can be called in exactly the same way as a built-in function.
The return line says what the output of the function will be.

  • The returned output value can then be used as a variable or printed.

def sq(x):
    return x**2

print(sq(2))
u = 3.5
v = sq(u+0.5) - 1.0
print(v)

Exercise:

Define the function \(f(x) = \dfrac{x^2}{3 + x}\).

  • Determine the quantities \(f(0)\), \(f(-1)\) and \(f(1/3)\).

The structure of the code will be like this:

#define function name and arguments here
def SOME_FUNCTION_NAME( INPUT_ARGUMENT_VARIABLE_NAME_TO_USE_IN_FUNCTION )
    #indented code for what to return to outside programme
    return OUTPUT_VALUE_TO_RETURN_TO_MAIN_PROGRAMME
# YOUR CODE HERE


print(f(0)) 

# perform other tests:
# YOUR CODE HERE

Result:

0.0 
0.5 0.03333333333333333

Click for Solution

Indentation of Code Blocks

The code run by the function must be indented by the same number of spaces.
This tells Python what is part of the function and what is outside it.
We can run many commands inside a function, including printing to the screen, rather than returning a value to be printed or used in a variable.

def FUNCTION_NAME(ARGUMENTS):
    #<-four spaces indented line, 
    # any code run by the function
    # when it is used 
    # needs to be in this indented block
    # anything outside the function
    # goes back to normal alignment
    return RETURNED_VALUE
    #anything after the return action is ignored...

def do_things(a_number):
    print("Your number is %.2f to 2DP" % a_number)
    print("Squared it is:", a_number**2)
    asqrt = a_number**0.5
    print("The square root of " + str(a_number) + " has been returned")
    return asqrt
    print("This does nothing because a return statement terminates the function!")

print("This is not inside the function!")
x = 5.499025
y = do_things(x)
print("This is after the function finishes!")
  • Notice: the last print() in the do_things() function is ignored because a function ends on a return statement!

print(y)

Note: When a function is used any arguments needed must be given in the brackets.
Also the name of the argument in the function definition a_number and that given as an argument when applying the function x are unrelated.

Exercise: Truncate Decimal Places

Write a function trunc5dp that truncates a number after the fifth decimal place. For example, trunc5dp(4321.1234567) = 4321.12345.

The steps (algorithm) for doing this are:

  1. Multiply by \(10^5\) (either written 100000 or 1e5), - e.g. \(4321.1234567\) becomes \(432112345.67\)

  2. use the int() function to remove trailing decimals, - e.g. \(432112345.67\) becomes \(432112345\)

  3. divide by \(10^5\). - e.g. \(432112345\) becomes \(4321.12345\)

the structure of the function is like this:

#function name definition, with 1 argument
    # indented code block
    # using algorithm
    # steps goes here
    # return the answer
# YOUR CODE HERE
    
ans = trunc5dp(4321.1234567)
print(ans)

Expected Result: 4321.12345

  • Does your function do what you expect for negative numbers?

print(trunc5dp(-4321.1234567))  # truncates (round towards zero)

Expected Result: -4321.12345

Click for Solution

Naming arguments

The names that you give parameters in the definition of the function are only used inside the function.
They do not relate to the arguments and variable names in the main program.

def myfunc(temporary_variable_name):
    answer = temporary_variable_name * 2
    return answer


x = 10

# x will be renamed temporary_variable_name inside the function (and only there)
y = myfunc(x)

print(y)
print(temporary_variable_name)  # this will lead to an error
print(answer) #so will this
  • Think about this…

Variable names used inside functions do not affect variables used in the rest of the program:

a = 1

def root(a):
    a = a**0.5 #this a is only a tempory name
    return a

b = 2

print(root(b))
print(a)
  • Here a is a different thing inside to outside the function. Make sure you are clear on this.

Avoiding confusion with parameters that are not arguments

Be careful using parameters that are not passed into the function as arguments, as this can cause confusion.

  • The following will cause an error when trying to apply (“call”) the function not at the time of defining it!

def funky(ar):
    ans = ar+bee
    return ans

print("Here!")

funky(20)

Understanding the error message:

Note the first part of the error message that points to the origin of the problem on line 7:

----> 7 funky(20)

which then traces the issue back to the function definition on line 2:

<ipython-input-1-40d916a0d279> in funky(ar)
      1 def funky(ar):
----> 2     ans = ar+bee
      3     return ans

before finally saying what was not understood by the Python interpreter:

NameError: name 'bee' is not defined

Avoiding the error

Either pass all parameters into the function as arguments, or make sure they are definitely defined before the function is called:

def funky(ar):
    ans = ar+bee
    return ans

bee = 8222

funky(778)

Exercise: Calculating Area

  • Complete the following program by writing a functions to calculate the area of a circle, given its radius (\(a = \pi r ^2\));

## Instructions: your code should have this structure:

# def circle_area(r):
#     return ??
from math import pi

# YOUR CODE HERE

print(circle_area(2.0))

Expected Result: 12.566370614359172

Click for Solution

Example: No Parameters

def the_answer():
    return 42


ans = the_answer()
print(ans)

Example: Multiple Parameters

def quadratic_value(a, b, c, x):
    "Evaluate a quadratic polynomial a*x**2 + b*x + c"
    return a*x**2 + b*x + c


help(quadratic_value)
print(quadratic_value(2.0, 3.0, 4.0, 0.5))

If a function requires several parameters, we need to separate the parameter names by commas.

  • The example also shows the use of a documentation string, which can be useful for the user.

  • The help function will print the documentation string, as well as the name of the function and the function parameters.

Local Variables

Any variables that are assigned inside a function definition are called local variables, and only exist inside the function.
This means that any local variables that we use for intermediate calculations inside a function will not interfere with the variables in the rest of the program.

Study and understand what is going on in the following example code blocks:

import math


def ellipse(a, b):
    ab = a*b
    print(ab)
    return math.pi*ab
ab = 4  # unrelated to local variable ab in ellipse
print(ab)
a = 2.0
b = 3.0

A = ellipse(a, b)
  • Note: ellipse() printed the internal value of ab to the output, but returned te value of \(\pi a b\) to the variable A:

print(A)

#the value of ab defined outside the function remains unchanged!
print(ab)

Multiple Return Values

A function can return several values, by separating the values by commas in the return statement.

The following example shows a function that returns the two roots of the quadratic equation \(a x^2 + b x + c = 0\).

import math


def quadratic(a, b, c):
    "return the roots of the quadratic equation a*x**2 + b*x + c = 0."
    discriminant = b**2 - 4*a*c  # assumed to be positive
    offset = math.sqrt(discriminant)
    x1 = (-b + offset)/(2*a)
    x2 = (-b - offset)/(2*a)
    return x1, x2


print(quadratic(4.0, 0.0, -16.0))

We can assign names to each of the return values using the following syntax:

root1, root2 = quadratic(4.0, 0.0, -16.0)
print(root1)
print(root2)
  • Note that the names of the local variables (x1 and x2) do not have to be the same as the variables we use to name the results of the function (root1, root2).

Exercise: Calculating perimeter and area of a square

  • Define a function to calculate the perimeter and area of a square, given the side length (\(P = 4L\), \(A=L^2\));

# YOUR CODE HERE

print(square_geometry(3))

Expected Result: (12, 9)

The cell below will test the function you defined above on a (hidden) test case

  • Only works on ACE Jupyterhub in your AR10366 folder

# Run this block to test your function on a hidden test case!
# this will only work running on ace.jupyterhub.bath.ac.uk in AR10366 folder
import sys
sys.path.append('.checks')

import check03

check03.test0301(square_geometry)

Click for Solution

Exercise: Calculating Perimeter and Area of a rectangle

  • Write a function to return the perimeter and area of a rectangle, given the width and height (\(P=2W + 2H\), \(A=W\times H\)).

  • test your function with \(W=3\) and \(H=4\)

# YOUR CODE HERE

#set W and H here

perimeter, area = rectangle_geometry(width, height)
print(perimeter)
print(area)

Expected Result:

14
12
# Run this block to test your function on a hidden test case!
# this will only work running on ace.jupyterhub.bath.ac.uk in the AR10366 folder
import sys
sys.path.append('.checks')

import check03a
check03a.test0302(rectangle_geometry)

Click for Solution

No Return Values

It is possible to define functions that do not return any values at all. These are used to perform tasks such as printing, reading or writing files, accessing the internet, or plotting graphs.

def hello():
    print("Hello World!")


hello()

The function does not contain a return statement and therefore the function does not return a value, just a special value None.

greeting = hello()  # the action of printing will still be performed

print(greeting)

Optional Arguments and Keyword Arguments

Sometimes arguments can be given default values using an equals sign.

If that argument is not entered when applying the function it takes the default value.

def raisepower(base, exponent=2):
    return base**exponent


print(raisepower(10))
print(raisepower(10, 5))
print(raisepower(2, exponent=10))
  • Keyword arguments must appear after compulsory arguments that do not have default values.

  • If there are more than 3 or 4 arguments it is a good idea to have them as optional keyword arguments with default values.

Functions names as arguments

Functions can also be used as input parameters (arguments) in Python.

from math import cos, sin, tan


def f_zero_ten(InputFunction):
    print(InputFunction(0.0), InputFunction(10.0))


f_zero_ten(cos)
f_zero_ten(sin)
f_zero_ten(tan)

Note: the function name is passed without parentheses ( )

  • if you input a function with arguments e.g. cos(0) then only the numerical value returned by \(\cos(0)=1\) is seen by the function.

from math import cos

def f_zero(fname):
    return fname(0.0)

x=0

f_zero(cos(x))

Understanding the error:

The error is first captured on line 8 when we pass cos(x) as an argument to our f_zero function:

----> 8 f_zero(cos(x))

But the error really occurs when trying to call (apply) fname as a function:

----> 4     return fname(0.0)

The error message states:

TypeError: 'float' object is not callable
  • This is because we passed cos(x) with x=0 into our function, but \(\cos(0)\) is just the number 1 (a float: 1.0).

  • the floating point number 1.0 is not a function so cannot be used (“called”) as if it were a function: 1.0(0.0) does not mean anything.

We would see a similar error using (calling) any other non-function as a function:

import math

answer = "The answer is:" #the variable answer holds a string

answer(42) #trying to pass an argument to the variable attempts to use the above string as a function

Exercise:

  • Define a function named twice() that takes a function fun and a value xval as two arguments, then returns \(f(f(x))\)

? ?(?, ?):
    return ?(?(?))
# define your twice() function below:
# YOUR CODE HERE


#function to give to your twice() function
def two_times_x(x):
    return 2*x


# give the function two_times_x as an argument to the twice() function along with the value 10

#a2 = twice(???, ???)
# YOUR CODE HERE

print(a2) #this should be the value of two_times_x(two_times_x(10))

Expected Result: 40

# Run this block to test your function...
# this will only work running on ace.jupyterhub.bath.ac.uk
import sys
sys.path.append('.checks')

import check03

# Test 1. test function to give to your twice() function
def x_squared(x):
    return x**2

# Test 2. 
from math import sqrt
# the sqrt function will also be used to evaluate sqrt(sqrt(16))

check03.test0303(twice)

Click for Solution

Summary

Defining a function

def rootmeansquare(a, b):
    c = a**2 + b**2
    print(f"The sum of squares is {c}")
    m = c/2
    print(f"The mean of squares is {m}")
    return m**0.5

Calling a function

x = 6
y = 9
s = rootmeansquare(x, 2*y)  # calling a function

print(f"RMS = {s:.4}")

Output:

The sum of squares is 360
The mean of squares is 180.0
RMS = 13.42

Functions with more than one output (returned value)

def minmax(a,b,c):
    minval = min(a,b,c)
    maxval = max(a,b,c)
    return minval, maxval

mn,mx = minmax(69,42,404)
print("Smallest is:", mn)
print("Largest is:", mx)

Output:

Smallest is: 42
Largest is: 404

Functions with optional arguments

def sigfig4(value_to_use, print_string="Rounded value:"):
    "Function to round to 4 significant figures"
    print(print_string, f"{value_to_use:.4}")
    #Note: does not return a value!
    
r2 = 200**0.5 #square root 200
sigfig4(r2, print_string="sqrt(200) is:")
returned = sigfig4(2**0.5)
print(returned)

Output:

sqrt(200) is: 14.14
Rounded value: 1.414
None

Functions with function arguments

def func_difference(dummy_function_name, value1, value2):
    y1 = dummy_function_name(value1)
    y2 = dummy_function_name(value2)
    diff = y2-y1
    return diff
    
    
from math import sqrt

ans = func_difference(sqrt, 4, 9)
# inside func_difference the calculation is sqrt(9)-sqrt(4)

print(ans)

Output:

1.0

Pitfalls

Watch out for the following sources of errors:

  • Missing colon before indentation block.

  • Wrong or inconsistent indentation. Use for 4 spaces (not tabs) per indentation level.

Task 3 (2%):

Numerical Differentiation

Background

Differentiation of a function \(f(x)\) about any point \(x=a\) can be defined as follows: $\(f'(a) = \lim\limits_{\delta x \rightarrow 0} \frac{f(a + \delta x) - f(a)}{\delta x}\)$

Therefore a numerical approximation to the gradient at a point \(a\) can be obtained by evaluating f(a)and f(a+dx) after some small increment \(\delta x\) (which we name as dx for simplicity)

The code below uses this technique to differentiate the specific function for the area of a cube:
\(A=6 L^2,\)
with side length \(L=2\)

def area_cube(L):
    area = 6.0 * L**2.0
    return area
 
a = 2 #value of x=a to differentiate at
dx = 0.1 #delta x
    
a1 = area_cube(a)
a2 = area_cube(a+dx)

f_x = (a2 - a1)/dx #differentiation formula

print(f_x)
  • Look back at "floating point precision" in week 1 to explain the last digit.

PART 1 (warm-up exercise, do not submit this)

  • Copy the code above and change change the value of \(\delta x\) to 0.01, 0.001 and 0.0001 to see how it affects the numerical approximation to the true value of 24.

def area_cube(L):
    area = 6.0 * L**2.0
    return area


x = 2
# YOUR CODE HERE
    
a1 = area_cube(x)
a2 = area_cube(x+dx)

f_x = (a2 - a1)/dx #differentiation formula

print(f_x)

PART 2 (for submission):

Write a function that can take any function as an argument and differentiate it

Step 1

Using the notes to guide you:

  • define a new function called diff() with three arguments: func, a and dx.

  • the diff() function should evaluate r1 = func(a) and r2 = func(a+dx) and use these values in the formula for the gradient given in the Background and call it grad (or whatever you like).

  • The diff() function should return the value of grad

Step 2

Test your diff() function by giving it the test_function(x) that returns the result of \(g(x) = x^3 - 2x^2 - 5\)

  • Give the test function name as the first argument to your diff() function to differentiate it at x=10 and dx=1e-3 (or 0.001).

# here define a function called diff(internal_function_name, OTHER, ARGUMENTS):
# YOUR CODE HERE

# leave this function as it is here
def test_function(x):
    return x**3 - 2*x**2 - 5

a=10
dx=1e-3 

#answer = diff(???)# apply your diff function here with arguments of the test function name, the value of x and the step-size.
# YOUR CODE HERE

print(answer)

Expected Result: 260.02800099990964

  • precision may vary very slightly on different computers, don’t worry about this if it’s OK below.

  • restart the kernel and check again!

  • Use the tests below to check it will pass the autograder

Once it’s working, put the code from the cell above into a new python script, check in Spyder (etc.) it works as expected from scratch and submit.

Use the test cell below to check that your function will pass the grading script:

  • Only works on ACE Jupyterhub in your AR10366 folder

# this will only work running on ace.jupyterhub.bath.ac.uk in AR10366 folder
import sys
sys.path.append('.checks')

import check03

#Test 1. Try the test function 
def test_function(x):
    return x**3 - 2*x**2 - 5
# at the values:
a=10
dx=0.001

#Test 2. Differentiating another (secret) function at another value

check03.testASS03(diff)