6  Defining Functions and Conditional Execution

6.1 Introduction

We have used a number of Python functions so far, such as the absolute value function and the square root function. In this chapter we will learn how we can create our own functions. We will also learn how to use conditional statements inside functions.

6.2 Structure of a Function

We will start off learning how to program a very basic function. Consider the function that returns its input plus one. Mathematically the function would be represented as:

f(x)=x+1 So f(0)=1, f(1)=2 and f(2)=3 and so on. In Python we can create this function with:

def add_one(x):
    y = x + 1
    return y

The def tells Python we are creating a function. We then provide the functions name (here add_one). After that we put in the function’s arguments in parentheses, separated by commas. Here there is only one argument so we just write x. Then like with a for loop we add a : and add the body of the function below it indented by 4 spaces. Here the only thing the function does is create y which is x + 1. We then get the function to return the output, which is y.

Let’s try it out:

add_one(2)
3

We get the expected output!

One thing to note about this function is that the y that is assigned x + 1 in the function is never stored in our environment. The y only exists within the function and is deleted after the function ends. We cannot access it outside. We say that y is a local variable (it is local to the function). It is possible to define global variables within a function that can be accessed after the function is called, but for our purposes doing so is generally not very good practice and so we will not cover that here.

We could also shorten our code by doing the calculation on the same line as the return command:

def add_one(x):
    return x + 1

6.3 Commenting in Python

As we start to write longer programs that include functions, it’s s good idea to start annotating your code to help other people understand its purpose (and also you when you look back at your own code after a couple of days!). We can do this by adding comments. In Python we can add a comment by using the # character. Everything after the # character is ignored by the Python interpreter, so what we write after it don’t need to be “legal” Python commands. We can add a comment to describe what a function does like this:

# This function returns the input plus one:
def add_one(x):
    return x + 1

We can also add comments to the same line as code we want to run provided we put it after the command. Like this:

2 ** 3  # this command calculates 2 to the power of 3
8

6.4 Conditional Execution

6.4.1 If-Else Statements

Conditional statements, or “if-else statements”, are very useful and extremely common in programming. In an if-else statement, the code first checks a particular true/false condition. If the condition is true, it performs one action, and if the condition is false, it performs another action.

A simple example of this is the absolute value function we saw in Chapter 3. Let’s define precisely what that function does:

|x| = \begin{cases} -x & \text{ if } x < 0 \\ x & \text{ otherwise} \end{cases}

If x<0 it returns -x, so that the negative number turns positive. Otherwise (if x=0 or it is positive), it keeps the value of x the same.

Although Python already has an absolute value function (abs()), let’s create our own function (called my_abs()) that does the same thing. To do this we use conditional statements (if and else). Here’s how it works:

def my_abs(x):
    if x < 0:
        y = -x
    else:
        y = x
    return y

The function first checks if x<0. If it is true, it performs the operation under if (sets y=-x) and skips past the else statement and returns y. If it is false (i.e. x \not < 0) then it skips past the operation under the if statement and instead does the operation under the else statement (sets y=x) before returning y.

Let’s try it out using some different values:

[my_abs(i) for i in [-2, 0, 3]]
[2, 0, 3]

Just like with the add_one() function above we can shorten this function definition. We could alternatively do:

def my_abs(x):
    if x < 0:
        return -x
    else:
        return x

The function first checks if x<0. If it is true, it returns -x and ends. It doesn’t go any further. If x \not < 0 then it skips the operation under if and does the operation under else (returns x).

This gives the same output:

[my_abs(i) for i in [-2, 0, 3]]
[2, 0, 3]

This means the return part of a function doesn’t have to be at the end of a function. But you should be aware that once a function returns a value it does not continue executing the remaining statements.

For example, consider the following code:

def bad_add_one(x):
    return x
    y = x + 1
    return y
[bad_add_one(i) for i in [1, 2, 3]]
[1, 2, 3]

This is very similar to the first add_one() function we defined above. The only difference is that we write return x as the first command in the function’s body. Although the code sets y=x+1 and returns y, the output is always the same as the input. This is because the function returns x at the top, which means the rest of the function is never executed.

6.4.2 If-Else If-Else Statements

Sometimes we want to do one thing if a certain condition holds, another thing if a different condition holds, and something else in the remaining cases. An example of this is the “sign” function, which tells you the sign in front of a value:

sgn(x) = \begin{cases} -1 & \text{ if } x < 0 \\ 0 & \text{ if } x = 0 \\ +1 & \text{ otherwise} \\ \end{cases} If the value is negative, we get -1. If it’s zero we get 0. If it’s positive (the remaining case), we get +1.

To do this in Python, we could nest several if-else statements:

def sign(x):
    if x < 0:
        return -1
    else:
        if x == 0:
            return 0
        else:
            return 1
[sign(i) for i in [-2, 0, 3]]
[-1, 0, 1]

The function does the following:

  • If x<0, return -1.
  • Otherwise proceed to the next if-else:
    • If x=0, return 0.
    • Otherwise (if x>0), return 0.

Although this works, this is quite complicated and difficult to follow. Some of the return statements are indented 4 times, for what should be such a simple function. For these kinds of situations we can make use of the elif statement. Here is an alternative way to make this function using elif:

def sign(x):
    if x < 0:
        return -1
    elif x == 0:
        return 0
    else:
        return 1
[sign(i) for i in [-2, 0, 3]]
[-1, 0, 1]

In words, what the code does in this case is:

  • If the 1st check is true (x<0), the function returns -1 and it done.
  • If the 1st check is false (x\not <0), the function checks x=0. If that is true it returns 0 and it done.
  • If the 1st and 2nd checks are false (x\not <0 and x\neq 0), the function returns 1 and it done.

It actually does exactly the same as the first code, but because there is less nesting it is easier to follow and is preferred (especially when you have even more conditions to check!).

6.4.3 While Loops

A while loop is another very common method in programming. A while loop repeats a set of commands whenever a certain condition is true. A while loop also makes it possible to run an infinite loop. To have the sequence of numbers 1, 2, 3, \dots printed on your screen forever (or until you kill the program) you could do:

x = 0
while True:
    x += 1
    print(x)

Note: the x += 1 here is a shorter way of writing x = x + 1. The operations -=, *= and /= also work like this - try them out!

Because True will always be True, the loop will just keep running forever, adding 1 each time. Eventually the numbers will get so big that x will show up as inf (infinity), but you would have to let the program run for a very long time before you saw that.

A while loop is also useful if you want to repeat a loop until something happens, but you don’t know how many times you need to run it before that happens. One instance when this would occur is if you wanted to numerically approximate a mathematical equation with an iterative algorithm. You want to repeat the iterations until the output starts to stabilize to a certain tolerance (accuracy) level, but you don’t know in advance how many iterations this will take. Let’s take a look at an example of this now.

The example we will look at is the way the ancient Greeks approximated square roots. Suppose you wanted to find the square root of x. What the ancient Greeks did is start with an initial guess of this y_0, let’s say \frac{x}{2}. You then find the updated guess y_1 according to the formula: y_1 = \frac{1}{2} \left( y_0 + \frac{x}{y_0}\right) When you have y_1 we can update this for a more accurate approximation with: y_2 = \frac{1}{2} \left( y_1 + \frac{x}{y_1}\right) To write this in general terms, given an initial guess y_0, we update y_n, with n=1,2,\dots according to: y_n = \frac{1}{2} \left( y_{n-1} + \frac{x}{y_{n-1}}\right)

You continue updating this way until y_n stops changing very much (for example it changes by less than 0.000001 in an iteration).

Let’s work manually with this algorithm to see how well it works. Suppose we want the square root of 2 which we know is approximately equal to 1.414214. Let’s start with a guess y_0=\frac{x}{2}=\frac{2}{2}=1. This is quite far off the true 1.414214 but we’ll go with it anyway. We can update the guess with the formula:

y_1 = \frac{1}{2}\left(y_0 + \frac{x}{y_0}\right) = \frac{1}{2}\left(1 + \frac{2}{1}\right) =1.5 This is already a lot closer (0.0858 away). Let’s do the next approximation step:

y_2 = \frac{1}{2}\left(y_1 + \frac{x}{y_1}\right) = \frac{1}{2}\left(1.5 + \frac{2}{1.5}\right) = 1.416667 This is already pretty close (0.00245 away)! Let’s do one more:

y_3 = \frac{1}{2}\left(y_2 + \frac{x}{y_2}\right) = \frac{1}{2}\left(1.416667 + \frac{2}{1.416667}\right) = 1.414216 It’s now only 0.0000021 away from the precise answer! That might be close enough for most purposes, and we can always do another iteration to improve its accuracy.

Let’s see how to code this in Python:

def my_sqrt(x, tol=0.000001):
    # Arguments:
    #  x   : number to take the square root of.
    #  tol : tolerance level of algorithm.
  
    # Set initial guess:
    y = x / 2
  
    # Initialize distance:
    dist = tol + 1 
    
    # Update guesses until y changes by less than tol:
    while dist > tol:
        # Previous guess:
        y_old = y
        # Update guess:
        y = (y_old + x / y_old) / 2
        # Calculate distance from last guess:
        dist = abs(y - y_old)
    return y

We have written a function that can take 2 arguments: x, the number we want to take the square root of, and tol, which is the tolerance level for how accurate our approximation should be (a lower number is more accurate). When we write tol=0.000001 in the function definition it means we say that 0.000001 is the default value for tol. If we don’t provide the argument it will use this value, but we can specify a different value if we want.

We now talk about the code in the function. The function first sets y_0=\frac{x}{2} as the initial guess. It also needs to set dist = tol + 1 because the while loop checks if dist > tol. For this check, dist needs to exist locally in the function (tol is created from the arguments). And for the while loop to run at least once the dist needs to start at a value bigger than tol. This is why we add one. Inside the while loop then, because we want to compare how our guess changes, we set y_old = y before setting the new y according to the approximation formula. Then we get the absolute value of the difference between the new and old guess. We then go back to the top of the loop and we check if dist is still bigger than tol. If it is, it repeats the steps again. If not, the while loop terminates and we go to the next stage, where y is returned as the output.

Let’s try it out. First using the default value:

my_sqrt(2)
1.414213562373095

We get an approximation that is very close to math.sqrt(2):

import math
math.sqrt(2)
1.4142135623730951

Can we specify a looser tolerance as follows:

my_sqrt(2, 0.1)
1.4166666666666665

As expected, this is less accurate.