Conditions and basic if statements#

In the previous worksheets, we’ve seen two ways in which we can control how the lines of a program are executed (which we sometimes call the control flow of the program): we can use for loops to repeat lines of code, and functions to separate lines of code into smaller blocks. There is only one more fundamental concept we need for manipulating the control flow. This is the ability to make decisions based on conditions.

In normal speech, when giving instructions we often use conditions. For example: “if the traffic light is red then stop”. This has a direct analogue in Python: the simple if statement, which has the following form:

if <condition>:
    body statements
next line of code

As always, the indentation is critical. The indented statements are the body of the if statement, and will only be executed if the condition holds.

In mathematical computing, the conditions we are interested in are often relationships between numbers. For example, we can test whether a is equal to b using a == b (notice there are two = signs).

In the following example, we print out when an iteratively-defined sequence has converged.

x = 0.9

for i in range(1, 10):
    x = 2*x*(1-x)
    if(x == 0.5):  # if the sequence has converged
        print(f"after {i} iterations, x = {x}")
after 8 iterations, x = 0.5
after 9 iterations, x = 0.5

The condition x == 0.5 evaluates to one of the two boolean values True or False:

x == 0.5
True
x == 1
False

Note that these are not the same thing as the strings "True" and "False"!

True == "True"
False

Relational and logical operators#

We’ve seen that you can use == to test equality. These is an example of a relational operator. There are several more relational operators available in Python.

Operator

Description

<, <=

less than, less than or equal to

>, >=

greater than, greater than or equal to

==

equal to

!=

not equal to

Here’s an example of using <= to find when the iteration above is “close to” converging.

x = 0.9
err = 0.05

for i in range(1, 10):  
    x = 2*x*(1-x)
    if(abs(x - 0.5) <= err):  # if the difference between x and 0.5 is small enough
        print(f"after {i} iterations, x = {x}")
after 4 iterations, x = 0.4859262511644672
after 5 iterations, x = 0.49960385918742867
after 6 iterations, x = 0.49999968614491325
after 7 iterations, x = 0.49999999999980305
after 8 iterations, x = 0.5
after 9 iterations, x = 0.5

Divisibility and the modulo operator#

One of the conditions that we often want to test is if an integer \(a\) is divisible by another integer \(b\). In Python we do this using the modulo operator a % b, which returns the remainder on dividing a by b:

a = 23
b = 5

a % b
3

You can then test if b divides a like so:

a % b == 0
False

As an aside, you can also get the quotient of a by b like so:

a // b
4

Logical Operators#

We often need to combine relational operators to form more complex conditions. For example, we may wish to test if a number is divisible by 13 and is positive. Python allows us to combine together conditions in this way:

n = 52
n % 13 == 0 and n > 0
True

There are three logical operators offered by Python:

Operator

Description

and

True if both sides evaluate to True

or

True if at least one side evaluates to True

not

the opposite of whatever follows it

Here are some examples of how they work:

13 % 2 == 0 or 27 % 5 == 2
True
13 % 2 == 0 or not 27 % 5 == 0
True

The example above is already a little confusing to read; it is good practice to bracket your logical expressions when they become complex.

(13 % 2 == 0) or not (27 % 5 == 0)
True

If-else statements#

It is very common in programming that we want the code to do one thing if a condition is true, and a different thing if it is false. In Python, we can achieve this using if-else statements, which have the following form:

if <condition>:
    lines to run if the condition is true
else:
    lines to run if the condition is false
lines to run regardless of condition

For example, suppose I want to check if I remember the value of \(\sinh(\log(2))\), and if not I want to be told the correct value.

import numpy as np

remembered_value = 5/4

if np.sinh(np.log(2)) == remembered_value:
    print("Hurray!")
else:
    print("Sorry, the actual value of sinh(log(2)) is", np.sinh(np.log(2)))
---------------------------------------------------------------------------
ModuleNotFoundError                       Traceback (most recent call last)
Input In [13], in <cell line: 1>()
----> 1 import numpy as np
      3 remembered_value = 5/4
      5 if np.sinh(np.log(2)) == remembered_value:

ModuleNotFoundError: No module named 'numpy'

Elif statements#

We often need our programs to choose between more than two options. Suppose we have a number \(n\) and we want to compute $\(x = \begin{cases}n^2 & \text{\)2\( divides \)n\(} \\ n^3 & \text{\)3\( divides \)n\( but \)2\( does not} \\ n & \text{otherwise.} \end{cases}\)$

In order to do so, we could nest if-else statements as in the cell below. This is just an example and would be considered bad practice, as explained afterwards.

# Don't do this! See the next text cell for a better method.

n = 3

if n % 2 == 0:
    # n is even, so set x = n ** 2
    x = n ** 2
else:
    # if we're here n isn't even, so we test if it is divisible by 3
    if n % 3 == 0:
        # n is not even but is divisible by 3, so set x = n ** 3
        x = n ** 3
    else:
        # n is neither even nor divisible by 3, so set x = n
        x = n
x
27

There are other ways of nesting the if-else statements to achieve the same result. However, nested conditionals are often extremely confusing and are a very common source of bugs in code. Sometimes it is necessary to nest conditionals, but in the example above a better alternative would be to use elif statements. These are used inside an if or if-else statement, and have the following general form:

if <condition 1>:
    thing to do if condition 1 holds
elif <condition 2>:
    thing to do if 1 does not hold but 2 does
elif <condition 3>:
    thing to do if 1 and 2 do not hold but 3 does
...
else:
    thing to do if none of the conditions hold

You can have as many elifs as you want, and you don’t have to include the else statement. Here’s what the code above would look like using elif:

n = 3
if n % 2 == 0:
    # n is even
    x = n ** 2
elif n % 3 == 0:
    # n is not even since the first condition failed
    # but it is divisible by 3
    x = n ** 3
else:
    # neither condition held, so n is neither even nor divisible by 3
    x = n
x
27

Conditional return statements#

We often combine if-else statements with return statements to determine what a function will return. The following function takes one argument n, and returns \(n/2\) if \(n < 100\) and \(2n\) otherwise.

def piecewise_function(n):
    if n < 100:
        return n/2
    else:
        return 2 * n
piecewise_function(5)
2.5
piecewise_function(100)
200

When writing functions which contain return statements, the key thing to remember is that as soon as a return statement is reached, the function returns the given value and the function stops. This is demonstrated by the following example, which returns the smallest (larger than 1) factor of a given number.

def smallest_factor(n):
    # go through the values 2 up to n in order, until we find a factor
    for i in range(2, n + 1):
        # if i divides n, then return i and exit this function immediately
        if n % i == 0:
            return i
smallest_factor(15)
3

When writing functions, we are not limited to a single return statement inside a function: we can have as many as we want. In the following example, we use this in combination with the fact that return statements exit the function to test if a number is prime. Make you sure understand this example!

def is_prime(n):
    # check whether the number has any factors (we could be cleverer about this range)
    for i in range(2, n):
        # if i divides n, then n is not prime (since 1 < i < n)
        # so return False and exit this function
        if n % i == 0:
            return False

    # If we get to this point, after the for loop, we haven't found any factors
    # because otherwise we would have already returned False and exited the function.
    # So n must be prime.
    return True
is_prime(17)
True

Aside: Patterns#

The function above follows a very common code pattern. A pattern is like a “template” for solving a particular type of problem. Here the more general problem is “non-existence of an object with some property”, which we can test by iterating through all of the possibilities for that object, returning False if one of them has the given property, and returning True after exhausting all of the possibilities. It’s worth thinking about what patterns you are using while you code. Can you see what the analogous pattern would be for checking “existence of an object with some property”? What about “all objects have some property”, or “all objects do not have some property”?