Lecture 3 (Loops)

Download the original Jupyter notebook

We finished last lecture by discussing the while loop, with a simple example like the one below:

a = 0
while a < 10:
    a = a+1
    print(a)
1
2
3
4
5
6
7
8
9
10

At the end of the lecture, we left as a homework two basic exercises. We will see here the solutions to those.

Few more basic operations on integers

Given two variables a and b of type int, we have seen that we can add, substract, and multiply those. There are two (three?) more basic operations: modulo operator %, takes two integers x and y, and returns the reminder of the division between x and y. For example:

9 % 3
0
10 % 3
1
11 % 3
2
12 % 3
0

For example, integer x is exactly divisible by y when x % y is zero. More generally, for $x, y >0$ if we can write $x = y * k + r$ for some integer $k$ and $0 \leq r < y$, then x % k is r.

We have discussed that = sign is used as an assignment operator. It is not asserting that two values are equal, neither it is checking if the two values are equal. Instead, it assigns the value of the expression on the right hand sign, to a variable on the left hand side.

How do we check then, if two values are equal? This is done using == operator: it takes two values, and outputs a value of type bool; True whenever the two values are equal, False otherwise

2 == 3
False
3 == 3
True

With this, we can write a simple expression that check if the value of a variable a is even: it is even if and only if the reminder of division a by 2 is equal to 0. For example:

a = 5
(a % 2) == 0
False
a = 6
(a%2) == 0
True

Let us try to use it in a slightly longer piece of code:

Exercise 1. Write a piece of code that prints all even numbers from $0$ to $99$.

a = 0
while a < 100:
    if a % 2 == 1:
        print(a)
    a = a + 1
1
3
5
7
9
11
13
15
17
19
21
23
25
27
29
31
33
35
37
39
41
43
45
47
49
51
53
55
57
59
61
63
65
67
69
71
73
75
77
79
81
83
85
87
89
91
93
95
97
99

Exercise 2. Write a piece of code that calculates the sum of all odd numbers from $0$ to $99$.

result = 0
a = 0
while a < 100:
    if a % 2 == 1:
        result = result + a
    a = a + 1
print(result)
2500

Note that in the solution above, we have introduced a new, and extremely important idea. We declared a new variable result, initially set to 0. As the while loop in our program is being executed, the variable result will keep the partial sum of all odd numbers up to the current iteration a. In each step, we can check if a is odd, if it is: we update the value of the variable result, by adding a to it. Regardless of whether a is odd, we increase a by 1.

Beware: The code below is an incorrent trying to solve the same problem. The a = a+1 instruction has been mistakenly put in the block inside the else: clause. Try to predict what is going to happen when attempting to execute this code?

result = 0
a = 0
while a < 100:
    if a % 2 == 1:
        result = result + a
    else:
        a = a + 1
print(result)
---------------------------------------------------------------------------
KeyboardInterrupt                         Traceback (most recent call last)
Cell In[32], line 5
      3 while a < 100:
      4     if a % 2 == 1:
----> 5         result = result + a
      6     else:
      7         a = a + 1

KeyboardInterrupt: 

Exercise 3. Factorial:

Write a program that starts with a variable n that has some integer value. At the end, variable result should have value $n! = 1 \cdot 2 \cdots \cdot n$

n = 4
result = 1
a = 1
while a <= n:
    result = result * a
    a = a + 1
print(n, "! is", result)
4 ! is 24

This is very similar to the code before. Note that result now starts with 1, and a starts with 1. (What would happen if either of those two variables were initialized to 0?)

How about division?

It might be natural to attempt to divide two integers a and b using the / operator. Let us see how is it working: dividing 5 by 2 gives us

5 / 2
2.5

Two and a half. That makes sense. Let us try one more time:

6/2
3.0

Six divides by two is three, that also looks reasonable. Note, however, that 3 has been printed as 3.0, not 3, why? Let’s check a type of the result of this division:

x = 6 / 2
type(x)
float

Even though 6 divides by 2 exactly, the / operation in Python is always a division that outputs a value of type float: this is useful, since without knowing what are the values of both things we are dividing, we can be certain about the type of this expression. But the float numbers are kept only up to some finite precision! If we keep using this result in follow-up computation, the errors might accumulate: even if mathematically the expressions we write always produce integer values, they will not be stored as integers.

For integer division, there is a different operator //, that always returns an integer.

6//2
3

Note that a result of this expression is printed as 3, not as 3.0. Let us check what is the type of this expresion:

x = 6//2
type(x)
int

What happens if we try to divide integer a by b, using the integer division operator //, when a is in fact not divisible by b? Let’s try:

16 // 3
5

We get the integer part of the division $11/3$. Since $16 = 3*5 + 1$, we have 16 // 3 == 5 and 16 % 3 == 1:

16 % 3
1

In short: The // operator applied to two values of type int always returns a value of type int. It is the whole part of the division. The / operator always returns the value of type float, regardless of whether the operands are int or float.

If you are trying two divide two integers, and know that one is divisible by the other, use // operator.

Exercise 4. Start with some value in a variable $n$. As long as this value is greater than $1$, keep repeating the following operation:

  1. If $n$ is even, divide it by two
  2. If $n$ is odd, multiply by three and add one.

Calculate the number of iterations it took to get to 1, and print it at the end.

n = 27
iterations = 0
while n > 1:
    iterations = iterations + 1
    if n % 2 == 0:
        n = n // 2
    else:
        n = 3*n + 1
print(iterations)
111

Exercise 5. Starting with two values in variables a and b, write a code that swaps the values in the two variables (i.e. after the code executes, we want the value of b to be what was originally in variable a, and value of a to be what what originally in variable b).

a = 22
b = 11

First, flawed attempt:

a = b
b = a

The code above of course does not work. Once the instruction a=b has been executed, the value that we had initially in the variable a is lost forever, and we do not know what to put in the variable b. The right way, is instead to introduce a new variable c, and store in it the original value of a.

a = 22
b = 11
c = a 
a = b
b = c
print("a is ", a, "b is ", b)
a is  11 b is  22

Seems to work. There is a more python-specific way of doing this:

a, b = b, a
print("a is ", a, "b is ", b)
a is  22 b is  11

For now, you can conisder a syntax: variable1, variable2, variable3 = expression1, expression2, expression3 as a code that first, simultanously, calculates all values of expressions on the right, and then assigns them to variables on the left, accordingly. This can be used, for example, to initialize few variables in a single line

a, b, c = 5, 10, 11
print(a)
5
print(b)
10
print(c)
11

Exercise 6. Fibonacci sequence is defined as $F_0 = 0, F_1 = 1$ and $F_{n+2} = F_{n+1} + F_n$. For example

$$\begin{align} F_0 & = 0 \\ F_1 & = 1 \\ F_2 & = 0 + 1 = 1 \\ F_3 & = 1 + 1 = 2 \\ F_4 & = 1 + 2 = 3 \\ F_5 & = 2 + 3 = 5 \\ F_6 & = 3 + 5 = 8 \end{align}$$

and so on. Write a program that starts with some integer value in the variable n, and prints all Fibonacci numbers up to the $n$-th.

n = 25
a, b = 0, 1
i = 2
while i <= n:
    a,  b = b, a+b
    print(b)
    i = i + 1
1
2
3
5
8
13
21
34
55
89
144
233
377
610
987
1597
2584
4181
6765
10946
17711
28657
46368
75025

The for loop

We have seen a similar pattern repeated very often in the lecture so far. Often, we write a loop because we want to repeat a specific block of code exactly n times. There is a specific Python construction that does exactly that. Instead of writing:

a = 0
while a < n:
    ...
    a = a+1

We can just write:

for i in range(n):
    ...

This loop will execute the code in the block exactly n times. In the first iteration, the value of the variable i will be 0, then it will be 1, then 2, and so on until n-1. Let us see it on an example:

n = 5
for i in range(n):
    print("i is ", i)
i is  0
i is  1
i is  2
i is  3
i is  4

This behaves slightly differently than the while loop above, in that if inside of the block, we modify the value of i, in the next iteration it is not going to be just increased by 1. Instead it will take the next value it was supposed to take. For instance, the following does not lead to infinite loop:

for i in range(n):
    print("i is ", i)
    i = 0
    print("i is now ", i)
i is  0
i is now  0
i is  1
i is now  0
i is  2
i is now  0
i is  3
i is now  0
i is  4
i is now  0

It is still considered bad form, though, so I recommend just never modifying the value of the variable used to control iteration of a loop (sometimes called iterator) inside the loop.

Let us try to use the for loop, to rewrite few of our previous exercises. First, calculating factorial:

n = 20
result = 1
for i in range(n):
    result *= i + 1
print(result)
2432902008176640000

Beware: Note that we are multiplying result by $i+1$, since $i$ goes from $0$ to $n-1$. If we instead multiplied by $i$, we would start by multiplying by zero, and the result will always be zero from this point on:

result = 1
for i in range(n):
    result *= i 
print(result)
0

Alternatively, we can also use syntax range(start, end) to iterate over the range which starts with start, and ends with end-1. The factorial code again:

result = 1
for i in range(1, n+1):
    result *= i
print(result)
2432902008176640000

And the Fibonacci code:

a, b = 0, 1
for i in range(2, n+1):
    a, b = b, a+b
    print(b)
1
2
3
5
8
13
21
34
55
89
144
233
377
610
987
1597
2584
4181
6765

Two more control flow instructions: continue and break

Inside a loop, if our program ever encounters continue instruction it will skip immediately to the next iteration of the loop. For example we can use this code to print all odd numbers in the range:

for i in range(n):
    if i%2 == 1:
        print(i)
1
3
5
7
9
11
13
15
17
19

But we could instead skip to the next iteration, whenever we see an even number. The following code will have the same effect as the one above:

for i in range(n):
    if i%2 == 0:
        continue
    print(i)
1
3
5
7
9
11
13
15
17
19

On the other hand, we can use break instruction to break out of the current loop: after encountering the break instruction, the Python will continue with the next instruction right after the loop. Let us see it on a silly example: if we want to find the smallest number larger than $0$ that’s divisible by $17$ we can just iterate from $1$ to $n$, and as soon as we see a value visible by $17$, we can break:

for i in range(1, n):
    print("i is now", i)
    if i % 17 == 0:
        print("Found i:", i)
        break
print("I am outside of the loop!")
i is now 1
i is now 2
i is now 3
i is now 4
i is now 5
i is now 6
i is now 7
i is now 8
i is now 9
i is now 10
i is now 11
i is now 12
i is now 13
i is now 14
i is now 15
i is now 16
i is now 17
Found i: 17
I am outside of the loop!

Let’s try to use this knowledge on an exercise:

q = 23

Exercise 7. Given $q > 1$, check if $q$ is prime. A positive number is composite if it can be written as $q = ab$ for $a, b >1$ integers. A number is prime if it is greater than $1$ and not composite.

is_composite = False
for i in range(2, q):
    if q % i == 0:
        is_composite = True
        break

if is_composite:
    print(q, "is composite")
else:
    print(q, "is prime")
23 is prime

With this code in hand, we can now try to produce an even more complex code. Note that by itself, that would have been a fairly complicated exercise, requiring code with a fairly complex logic. Once we know how to check if a number is prime, we can just copy-paste this code, and wrap with one more loop, to solve the exercise below:

Exercise 8. Print all prime numbers from $2$ to $n$.

n = 100
for q in range(2, n + 1):
    is_composite = False
    for i in range(2, q):
        if q % i == 0:
            is_composite = True
            break
    
    if not is_composite:
        print(q)
2
3
5
7
11
13
17
19
23
29
31
37
41
43
47
53
59
61
67
71
73
79
83
89
97

Speed up: note that if a number $q$ is composite, it can be written as $q = ab$. For $a, b > 1$ integers. But then either $a$ or $b$ is at most $\sqrt{q}$. As such, when checking if a number is prime, once we are trying a potential divisor $i$, such that $i^2 > a$, if we haven’t found a divisor yet, we can be sure that we will not find one. This leads to a significant speedup:

The code for checking if $q$ is prime can be improved like this:

is_composite = False
for i in range(2, q):
    if i*i > q:
        break
    if q % i == 0:
        is_composite = True
        break

if is_composite:
    print(q, "is composite")
else:
    print(q, "is prime")
100 is composite

And similarly the code for listing all primes:

for q in range(2, n + 1):
    is_composite = False
    for i in range(2, q):
        if i*i > q:
            break
        if q % i == 0:
            is_composite = True
            break
    
    if not is_composite:
        print(q)
2
3
5
7
11
13
17
19
23
29
31
37
41
43
47
53
59
61
67
71
73
79
83
89
97