Loops#
In lab 2 we were introduced to data structures like lists, tuples and dictionaries. These collections allowed us to group multiple pieces of data together:
temperatures = [200, 220, 240, 260, 280, 300, 320, 340, 360, 380, 400]
Often, we would like to perform the same calculation on every element of a list. With reference to the example above, let’s imagine that we require the square of the temperatures (\(T^{2}\)):
squared_temperatures = [temperatures[0] ** 2,
temperatures[1] ** 2,
temperatures[2] ** 2,
temperatures[3] ** 2,
temperatures[4] ** 2,
temperatures[5] ** 2,
temperatures[6] ** 2,
temperatures[7] ** 2,
temperatures[8] ** 2,
temperatures[9] ** 2,
temperatures[10] ** 2]
print(squared_temperatures)
[40000, 48400, 57600, 67600, 78400, 90000, 102400, 115600, 129600, 144400, 160000]
This code will run and does indeed calculate the squared temperatures from our original temperatures
list, but it is hardly the cleanest approach. For one thing, this method is not scalable, here we have \(11\) temperatures - what if we had 1000? Moreover, this code is very error-prone, it would be very easy for a small typo in one line to cause a problem further down the line e.g. what if we accidentally used the wrong index for one of the temperatures?
for
loops#
A better approach to the problem outlined above is to use loops.
for T in temperatures:
print(T ** 2)
40000
48400
57600
67600
78400
90000
102400
115600
129600
144400
160000
Let’s unpack this code. The output matches our previous attempt to calculate the squared temperatures, so clearly this approach is ultimately doing the same thing: calculating \(T^{2}\) for each temperature in our list
of temperatures
. This is an example of a loop, or more specifically a for
loop. As the name suggests, a for
loop allows us to loop over every element of a list and to do something for
each one. The general syntax reads something like:
for item in iterable:
do_something_with_each_item
An iterable is simply any object that can be iterated i.e. it contains a sequence of items that can be accessed one after another. In our example, the temperatures
list is our iterable: it is a sequence of numbers that can be iterated through in a logical order.
Okay, so the first line in our loop example is: for T in temperatures
. As we just discussed, temperatures
is our iterable, so the latter part of this line (in temperatures
) reads as “We are going to loop through each element of the temperatures
list”. The former part of the line (for T
) tells us that at each iteration of the loop, we will assign the next item in temperatures
to the variable T
.
After ending the first line with a colon :
, the second line is indented beneath. This indentation is usually achieved with either four spaces (just pressing the spacebar four times) or a single tab character (just pressing the tab key once). The second line describes what we would like to do with each element of our iterable. In our example, each element is a temperature from the temperatures
list, which is assigned to the variable T
at each iteration of the loop. We would like to calculate the squared temperature, so we print T ** 2
.
So, to summarise, at the first iteration of our loop, T
is assigned to the first element of temperatures
:
T = temperatures[0]
Then the second line runs:
print(T ** 2)
Then we reach the second iteration of the loop, so T
is assigned to the next element of temperatures
:
T = temperatures[1]
And so on and so forth. The variable assignements as written above are implicit in the general syntax of a for
loop.
To run more code after the loop has been completed, we remove the indent:
for T in temperatures:
print('Still in the loop.')
print(T ** 2)
print('The loop has now finished!')
Still in the loop.
40000
Still in the loop.
48400
Still in the loop.
57600
Still in the loop.
67600
Still in the loop.
78400
Still in the loop.
90000
Still in the loop.
102400
Still in the loop.
115600
Still in the loop.
129600
Still in the loop.
144400
Still in the loop.
160000
The loop has now finished!
All of our code so far uses the print
function to display the squared temperature for each element of temperatures
. This is not quite the same as the problem we originally posed, which was to create a new list of squared temperatures. We can replicate this functionality by taking advantage of an empty list:
squared_temperatures = []
for T in temperatures:
squared_temperatures.append(T ** 2)
print(squared_temperatures)
[40000, 48400, 57600, 67600, 78400, 90000, 102400, 115600, 129600, 144400, 160000]
Here the first line creates an empty list by using a set of square brackets with nothing in them []
. We then use the append
method to fill this list with the squared temperatures generated by our loop. We can see this happening more clearly by moving the print
statement inside the loop:
squared_temperatures = []
for T in temperatures:
squared_temperatures.append(T ** 2)
print(squared_temperatures)
[40000]
[40000, 48400]
[40000, 48400, 57600]
[40000, 48400, 57600, 67600]
[40000, 48400, 57600, 67600, 78400]
[40000, 48400, 57600, 67600, 78400, 90000]
[40000, 48400, 57600, 67600, 78400, 90000, 102400]
[40000, 48400, 57600, 67600, 78400, 90000, 102400, 115600]
[40000, 48400, 57600, 67600, 78400, 90000, 102400, 115600, 129600]
[40000, 48400, 57600, 67600, 78400, 90000, 102400, 115600, 129600, 144400]
[40000, 48400, 57600, 67600, 78400, 90000, 102400, 115600, 129600, 144400, 160000]
Hopefully it is apparent why this solution is superior to what we started with originally. We can scale this loop as much as we would like, it would work in just the same way whether our temperatures
list has 11 elements or 1000!
Keeping track of iterations#
In our previous examples we have made frequent references to which iteration of a loop we are currently on/in. Let’s look at a simple toy example:
some_numbers = (5, 10, 15, 20, 25)
for number in some_numbers:
print(number)
5
10
15
20
25
Here we are looping through a tuple
of \(5\) numbers: the first \(5\) multiples of \(5\). Given that there are \(5\) elements in some_numbers
, the for
loop goes through \(5\) iterations: one for each element of some_numbers
. What if we want to keep track of which iteration the loop is on? One solution would be to utilise an additional variable:
some_numbers = (5, 10, 15, 20, 25)
iteration_count = 1
for number in some_numbers:
print(f'This is iteration #{iteration_count}')
print(number)
iteration_count = iteration_count + 1
This is iteration #1
5
This is iteration #2
10
This is iteration #3
15
This is iteration #4
20
This is iteration #5
25
Here we create a variable iteration_count
and assign it a value of \(1\). We then print the value of this variable on every iteration of the loop. We also increment the value of this variable within each loop, so that it effectively tracks which iteration we are currently on. Note that, just like the example in lab 1, we can abbreviate the last line as follows:
some_numbers = (5, 10, 15, 20, 25)
iteration_count = 1
for number in some_numbers:
print(f'This is iteration #{iteration_count}')
print(number)
iteration_count += 1
This is iteration #1
5
This is iteration #2
10
This is iteration #3
15
This is iteration #4
20
This is iteration #5
25
This approach works just fine, but it’s a bit of a faff to declare and update a new variable every time we want to run a loop and keep track of each iteration. Python provides us with a convenient built-in function to get around this problem: the enumerate
function.
some_numbers = (5, 10, 15, 20, 25)
for iteration_count, number in enumerate(some_numbers):
print(f'This is iteration #{iteration_count}')
print(number)
This is iteration #0
5
This is iteration #1
10
This is iteration #2
15
This is iteration #3
20
This is iteration #4
25
This code gives us almost exactly same result as our previous effort, but without the need for the manual declaration of a new variable. The difference is that here the iterations are counted starting from zero, just like list
indices.
The enumerate
function pairs each value of a given iterable with an index. In our example above, we start with our tuple
of numbers:
some_numbers = (5, 10, 15, 20, 25)
Calling the enumerate
function on some_numbers
, we end up with (under the hood):
((0, 5), (1, 10), (2, 15), (3, 20), (4, 25))
These are pairs of values, where the first element of each pair is the iteration index and the second is the original value from some_numbers
. We are then assigning the first element of every pair to iteration_count
and the second to number
, hence:
for iteration_count, number in enumerate(some_numbers):
To further clarify this, let’s go through the first two iterations of this loop manually. The loop will start with the first element of enumerate(some_numbers)
, and will assign the first value of this pair to iteration_count
and the second to number
:
iteration_count, number = (0, 5)
Reminder
This is an example of multiple assignment, just like we saw in lab 2. We can assign each element of a list
or tuple
to a variable using commas. In the example above, iteration_count
is assigned \(0\) and number
is assigned \(5\).
We then run the code inside the loop:
print(f'This is iteration #{iteration_count}')
print(number)
Then we move to the second iteration, so iteration_count
and number
are reassigned to the second element of enumerate(some_numbers)
:
iteration_count, number = (1, 10)
And so on and so forth.
It’s worth emphasising that in our particular example, we could force Python to start counting iterations from one rather than zero quite easily:
some_numbers = (5, 10, 15, 20, 25)
for iteration_count, number in enumerate(some_numbers):
print(f'This is iteration #{iteration_count + 1}')
print(number)
This is iteration #1
5
This is iteration #2
10
This is iteration #3
15
This is iteration #4
20
This is iteration #5
25
The only change we have made is to add \(1\) to iteration_count
every time it is displayed with the print
statement.
A shortcut to sequences of integers: the range
function#
Sometimes we want to loop over a simple sequence of integers. We could do this, as in our previous examples, like so:
integers = [0, 1, 2, 3, 4]
for integer in integers:
print(integer)
0
1
2
3
4
On the other hand, as is probably becoming familiar to you at this point, Python provides us with a shortcut via a built-in function. This time, we are interested in the range
function:
for integer in range(0, 5):
print(integer)
0
1
2
3
4
Which gives us exactly the same result, with one less line of code. The range function takes two arguments, an integer from which to start the sequence, and an integer at which to terminate the sequence. Note that, much like slicing lists, the latter is not included in the final sequence (here we supply 5
as the second argument, but the sequence terminates at 4
).
We can actually get away with shortening our code slightly more:
for integer in range(5):
print(integer)
0
1
2
3
4
If the range
function is only given one input, then Python assumes that you want the sequence to start from zero (again much like slicing lists). On the other end of the spectrum, we can also provide \(3\) arguments to range
:
for integer in range(0, 5, 2):
print(integer)
0
2
4
This third argument, once more just like list slicing, acts as a step size. Here we set it to 2
, so we get only the even-numbered integers. We can use a negative step size to loop through numbers in reverse order (from bigger to smaller):
for integer in range(5, 0, -1):
print(integer)
5
4
3
2
1
Exercises#
1.
a) Using a for
loop, generate and print
the first \(20\) square numbers.
Tip
You can generate integers from \(1\) to \(20\) with the range
function.
b) Modify your loop to keep track of the current iteration and print this along with each square number.
2. The Fibonacci sequence is defined by the recursive relation:
i.e. each term is simply the sum of the previous two terms.
Starting with the following list:
fib = [0, 1]
a) Using list indexing, calculate the next term in the sequence and append this to fib
.
Tip
Remember that you can use negative indices to access elements in a list in reverse order.
b) Write a loop to calculate the next \(10\) terms in the sequence and add these to fib
.