Loops

for loops

The first control flow statement we’ll look at is called a for loop.

Run the following code and see what happens

example_list = ['a', 'b', 'c', 'd', 'e']

for each_lettter in example_list:
    print(each_lettter)

You should see that the elements of the list are printed, one by one.

The above for loop loops over every element of the list and does something for each one - in our case it prints a value the screen.

NoteSyntax

The general syntax of a for loop is

for item in iterable:
    do_something_with_each_item
    do_something_else
    and_so_on...

where the code within the loop is indented by one tab (or four spaces), just like it was for functions.

An iterable is a Python object that can be iterated over, i.e. it contains a sequence of items that can be accessed one after another. So far in this course, the iterables we’ve seen are dict, list, tuple, and str.

Use a for loop to print the wavelengths of the first ten lines in the Lyman series of the hydrogen atomic emission spectrum.

Use the Rydberg equation

as implemented in the function below, noting that \(n_f=1\) for the Lyman series.

from scipy import constants
def calc_rydberg_wavelength(n_i, n_f):
    '''
    Calculates wavelength of hydrogen emission signal corresponding to a transition
    between two energy levels with quantum numbers 
    n_i and n_f using the Rydberg equation.

    λ = (R (1/n_f^2 - 1/n_i^2))

    Arguments
    ---------
    n_i: int
        Quantum number of intial state (n_i>n_f)
    n_f: int
        Quantum number of final state (n_i>n_f)

    Returns
    -------
    float
        Wavelength of transition in nm
    '''

    iwavelength = constants.Rydberg * (1/n_f**2 - 1/n_i**2)

    wavelength = 1 / iwavelength
    wavelength *= 10**9

    return wavelength

Only the last block of code needs to change, i.e.

print('Lyman series of Hydrogen')

n_f = 1
n_i_values = [2, 3, 4, 5, 6, 7, 8, 9, 10]
for n_i in n_i_values:
    wl = calc_rydberg_wavelength(n_i, n_f)
    print(f'n={n_i:d}-->n={n_f:d} λ={wl:.2f} nm')

Iterations

As we loop over the elements of an interable, it might be useful to keep a track of how many times the loop has run. We call this the number of iterations. The simplest way would be to use a variable that keeps track of the iterations

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

We start with a variable iteration_count=1, and every time the loop runs this variable is printed, and then its value is increased or incremented by 1. This means that iteration_count then tracks which iteration the loop is currently on.

This works, but it’s messy. Luckily, Python gives us a built-in function which does this for us - enumerate.

some_numbers = (5, 10, 15, 20, 25)

for iteration_count, number in enumerate(some_numbers):
    print(f'This is iteration #{iteration_count:d}')
    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 gives us almost exactly the same result as before, but with one key difference - here the iterations are counted starting from zero, just like when indexing a list or tuple.

Modify the print call in the above for loop so that the iteration counter starts at 1 instead of 0

print(f'This is iteration #{iteration_count+1:d}')

The enumerate function pairs each value of a given iterable with an index and provides a way to keep track of the iteration count.

NoteSyntax

The enumerate function can be used in a loop as follows

for counter, item in enumerate(iterable):
    do_something_with_each_item_or_counter
    do_something_else
    and_so_on...

where counter indicates the iteration number of the loop, and item is an element of iterable.

Ranges

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 three 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

Multiple iterables

How might we write a for loop to calculate the element-wise product of the following two lists?

evens = [1, 3, 5, 7, 9]
odds = [0, 2, 4, 6, 8]

Python provides us with a function that takes in two or more iterables and returns the corresponding number of values in each iteration of a loop. This function is called zip, and can be used to solve the above problem.

for even, odd in zip(evens, odds):
    print(even * odd)
0
6
20
42
72
NoteSyntax

The zip function can be used in a loop as follows

for value_1, value_2, ... in enumerate(iterable_1, iterable_2, ...):
    do_something_with_each_item_or_counter
    do_something_else
    and_so_on...

where value_1 is an element of iterable_1 and so on.

Nested loops

Just as we saw that we can have nested lists (e.g. a list in a list) we can also have nested loops.

Run the following code and see what happens

list_1 = [1, 2, 3]
list_2 = ['a', 'b', 'c']

for item_i in list_1:
    for item_j in list_2:
        print(item_i, item_j)

You should see that the elements of list_1 and list_2 are printed with a very particular order.

1 a
1 b
1 c
2 a
2 b
2 c
3 a
3 b
3 c

To understand what’s going on inside of these loops, let’s use enumerate and an additional print statement.

list_1 = [1, 2, 3]
list_2 = ['a', 'b', 'c']

for iteration_count, item_i in enumerate(list_1):
    print(f'Outer loop iteration #{iteration_count}')
    for item_j in list_2:
        print(item_i, item_j)
Outer loop iteration #0
1 a
1 b
1 c
Outer loop iteration #1
2 a
2 b
2 c
Outer loop iteration #2
3 a
3 b
3 c

We’re now printing every time there is an iteration of the outer loop.

So the outer loop iterates three times, and inside each three lines of data are printed. Let’s add another enumerate function call and print statement, but this time to the inner loop.

list_1 = [1, 2, 3]
list_2 = ['a', 'b', 'c']

for iteration_count_i, item_i in enumerate(list_1):
    print(f'Outer loop iteration #{iteration_count_i}')
    for iteration_count_j, item_j in enumerate(list_2):
        print(f'Inner loop iteration #{iteration_count_j}')
        print(item_i, item_j)
Outer loop iteration #0
Inner loop iteration #0
1 a
Inner loop iteration #1
1 b
Inner loop iteration #2
1 c
Outer loop iteration #1
Inner loop iteration #0
2 a
Inner loop iteration #1
2 b
Inner loop iteration #2
2 c
Outer loop iteration #2
Inner loop iteration #0
3 a
Inner loop iteration #1
3 b
Inner loop iteration #2
3 c

This might look a little bit confusing, but breaking it down step by step we see that

  1. The outer loop starts, printing its iteration number and values from list_1.
  2. Then the inner loop runs three times, each time printing its iteration number and values from list_2.
  3. The outer loop iterates again, and so on.

For every iteration of the outer loop, the inner loop iterates through the entirety of list_2. So the inner loop does all of its iterations before the outer loop iterates again.

Exercises

1a. 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.

1b. 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 recursion relation

\[F_{n} = F_{n - 1} + F_{n - 2},\]

i.e. each term is simply the sum of the previous two terms.

Starting with the following list:

fib = [0, 1]

2a. 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.

2b. Write a loop to calculate the next 10 terms in the sequence and add these to fib.