And again and again and again...

When we looked at vectors, we saw how python can treat a list of numbers all in one go, instead of doing them one at a time. Occasionally however, we come across problems where we need to go step by step. Take the simple example of calculating the terms of the series $T_n=T_{n-1}^2-3$, where $T_0=1$. Clearly $T_1$ depends on $T_0$, $T_2$ depends on $T_1$ and so forth, so we need to calculate the terms in turn, so that we can calculate successive terms.

A naive way to do this would be to code each term on a new line:

T0= 1
T1 = T0**2 - 3 
T2 = T1**2 - 3
T3 = T2**2 - 3
...

but what if we need T100, or T10000? We don't want to type 10000 lines of code, especially where each line is practically identical to the last.

Thankfully, computers are good at executing repeated commands. In python we can use a for loop to execute a piece of code repeatedly. A good parallel would be a shopping list.

shopping_list = ['chocolate', 'crisps', 'doughnuts', 'beer']

for item in shopping_list:
    find(item)
    pay_for(item)
    consume(item)

The loop structure is perfect, because we want to do exactly the same thing for each item: find it, pay for it and eat it. Notice that we have a variable called item which refers in turn to each item on the list. There's no reason it has to be called item, either: we could have

for x in [1,2,3]:
    print(x**2)

Let's look at the anatomy of the for-loop in more detail.

For-loops

The basic syntax for a for-loop is

for <iterator> in <list>: 
        <command 1>
        <command 2>
        <command 3>

    <normal command>

Let's take this piece by piece.

for

Typing the word for tells python we are going to start a loop. It is a special word in python, just like if or def, so you'll see that the editor highlights the word in blue when you type it

<iterator>

The <iterator> is the variable which changes each time we loop around, like the items on our shopping list. It is common to use i for this variable, as this is also common in mathematical notation: e.g. $\sum\limits_{i=1}^{10}$. I prefer to use ii because if your searching through a big piece of code i will appear in a lot of places- inside other words or variables, whereas ii will only appear where you've used it as an iterator. All of my examples will use ii but you can use whatever name you like.

in <list>

The list is a special type of array in python. Very often we will be using numpy arrays for loops, but python supports more general lists which contain strings (like the shopping list above) or even other lists. If you have a numpy array already defined you can use it as your list, e.g.

x= np.linspace(0,1,100)
y= np.sin(x)
for yval in y:
    print(yval)

or you can make the arrays on the fly:

for x in np.linspace(0,1,100):
    print(np.sin(x))

or you can just type in short lists inside square brackets like this:

for ii in [1,4,16,25]:
    x = np.sqrt(ii)
    print(x)

:

Just like with function definitions and if statements, the for loop statement has to be terminated with a colon. This tells python that the code following is the bit we want to loop. And also like functions and if statements, we indent everything after the colon that we want to be part of the loop. Hence this piece of code

for ii in np.arange(5):
    print ("Karma")

print ("chameleon")

"karma" gets printed five times, because it's 'in the loop' (indented) but "chameleon" gets printed just once, because it's 'outside the loop' (unindented).

Some Examples

In [16]:
for ii in [1,2,3]:
    print('hello')
hello
hello
hello

When python sees the for keyword, it takes the first value of the iterator (ii=1) and then executes all the commands in the for loop, then goes back, updates the iterator (ii=2), and performs the commands again etc. In this example, we haven't actually used the value of the iterator, but change the command from print('hello') to print(ii) and the code will print '1', '2', '3' instead.

In [17]:
for name in ['peter','paul','percy']:
    print('hello',name)
hello peter
hello paul
hello percy

Simple recursive formula

Let's take the example from the introduction: we have a series defined by $$T_n=T_{n-1}^2-3$$ and $T_0=1$. Let's say we want the value of $T_{100}$, we can implement this as follows:

In [3]:
T0=1
previous_term=T0
upper_limit=100
for ii in np.arange(1,upper_limit+1): # remember arange doesn't include the upper limit, so this does up to 100 and stops
    term=previous_term**2-3 # calculate the new term using the previous term
    previous_term=term      # update the value of the previous term for the next iteration
    
print('T',upper_limit, 'is', term)
T 100 is 1

Now this is easily verified, because if you work out the second term, it's $T_1=1^2-3=-2$. Then the next term is $T_2=(-2)^2-3=1$, so the series just oscillates back and forth between 1 and -2. Hence the 100th term is $1$ as given by our code, and we can verify this by changing the upper_limit variable in the code above. Also, we can change the code so that instead of printing out only the last term, on each step the code prints out the value.

In [5]:
T0=1
previous_term=T0
upper_limit=3
for ii in np.arange(1,upper_limit+1): 
    term=previous_term**2-3 
    previous_term=term
    print('T',ii, 'is', term)
T 1 is -2
T 2 is 1
T 3 is -2

Array elements

Recall from the arrays tutorial that we can reference an element of a particular array using square brackets.

In [8]:
x = np.arange(0,2,0.5)
print (x[0])
print (x[2])
print (x[-1])
0.0
1.0
1.5

For many applications we will be able just to treat the whole varray in one go, just like we did in the array tutorial e.g.

np.sin(x)

But what if we want to do something more sophisticated. For instance, we could take the average of each pair of values in x. (Recall that x will be of the form [0, 0.5, 1, 1.5]

In [9]:
for ii in np.arange(0,3,2):
    print (0.5*( x[ii] + x[ii+1] ))
0.25
1.25

Here instead of looping through the elements of x we create an iterator called ii which first takes the value 0 and then the value 2 (np.arange(0,3,2) starts at 0, ends before 3 in steps of 2). So we can trace through the loop. When ii=0 the code reads:

print (0.5 * ( x[0] + x[1]) )

and when ii=2 the code reads:

print (0.5 * ( x[2] + x[3]) )

You should be able to trace through a loop like this, even just for the first or last few terms to make sure you know exactly what your code is doing. Not also that you have to be careful not to reference elements of the array that aren't there.

In [10]:
for ii in np.arange(0,5,2):
    print (0.5*( x[ii] + x[ii+1] ))
0.25
1.25
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
<ipython-input-10-4ce7a583997e> in <module>()
      1 for ii in np.arange(0,5,2):
----> 2     print (0.5*( x[ii] + x[ii+1] ))

IndexError: index 4 is out of bounds for axis 0 with size 4

Here we get an error because when the iterator ii=4 we are trying to access x[4] whereas the last element of x is x[3].

Exercises

Once you have read this information then proceed to try the exercises on loops and lists.