More on Loops

Overview, Objectives, and Key Terms

In Lecture 8, the use of while loops in Python introduced to solve problems requiring iteration. The while loop structure in Python is very similar to structure introduced via pseudocode in Lecture_5. In this lecture, an additional for-loop construct is introduced, which reduces the amount of “bookkeeping” required in some cases. All loops can also be nested, which provides substantial flexibility when processing data, defining multidimensional arrays, and performing similar, potentially multidimensional tasks.

Objectives

By the end of this lesson, you should be able to

  • Use a for loop to solve simple problems using iteration
  • Use nested for loops to fill the elements of a multidimensional array.
  • Use the graphical debugger in Spyder to trace and debug a program with iteration

Key Terms

  • for
  • range
  • continue
  • loop variable
  • dependent loop variable

The for Loop

Remember once again the problem of printing out each element of an array a. In Lecture 8, the following solution was proposed using a while loop:

# print out the elements of an array using a while loop
import numpy as np
a = np.array([1, 1, 2, 3, 5, 8, 13])
n = len(a)
i = 0
while i < n:    # always remember the :
    print(a[i]) # indented 4 spaces
    i = i + 1   # also indented 4 spaces

The hallmark of such a while loop is the counter (here, the i). Defining a counter is not hard, but it’s a pain when we forget to update it (infinite loop), and there’s always a chance that the update is wrong. An alternative construct in Python (and other languages) that works for such “counter” problems is the for loop. Let me introduce this by example:

In [1]:
# print out the elements of an array using a for loop
import numpy as np
a = np.array([1, 1, 2, 3, 5, 8, 13])
for i in range(0, len(a)): # always remember the :
    print(a[i]) # indented 4 spaces
1
1
2
3
5
8
13

Some of this is identical to the while-loop solution: the same a and the same print. However, we don’t have the i = 0 initialization, nor is i updated inside the while loop. And, in place of while i < n, we have for i in range(0, len(a)). That is a very common pattern when using Python for loops. Here’s what range does:

In [2]:
range(0, len(a))
Out[2]:
range(0, 7)
In [3]:
type(range(0, len(a)))
Out[3]:
range
In [4]:
np.array(range(0, len(a)))
Out[4]:
array([0, 1, 2, 3, 4, 5, 6])

In other words, range is a built-in function and type (note the syntax color), but it represents a sequence of integers as shown by the conversion to an ndarray. The range function accepts three arguments: start, end, and stride, similar to slicing:

In [5]:
# just one argument means the end of the range
np.array(range(5))
Out[5]:
array([0, 1, 2, 3, 4])
In [6]:
# two arguments means the start and end
np.array(range(1,5))
Out[6]:
array([1, 2, 3, 4])
In [7]:
# three arguments means the start, end, and stride
np.array(range(0, 5, 2))
Out[7]:
array([0, 2, 4])

The for i in structure is not limited to range, though. In fact, i can come from any sequential type, like ndarray or the list and tuple types we’ll cover later on. Hence, we can also do

In [8]:
for i in np.arange(5):
    print(i)
0
1
2
3
4

and

In [9]:
for x in np.linspace(0, 1, 6):
    print("x = ", x)
x =  0.0
x =  0.2
x =  0.4
x =  0.6
x =  0.8
x =  1.0

A Pythonic Quirk

One reason that while loops were introduced first is because a for loop in Python actually follows a slightly different logic than an apparently identical while loop. Consider these examples:

In [10]:
n = 5
i = 0
while i < n:
    print("i = ", i)
    i += 2
i =  0
i =  2
i =  4
In [11]:
# A "quirky" for loop
for j in range(n):
    print ("j = ", j)
    j += 2
j =  0
j =  1
j =  2
j =  3
j =  4

Something is amiss. Both loops appear to take the counter i (or j) from 0 to 5 in jumps of 2. However, even though we set j to 2 after just one iteration of the for loop, the next time around, j appears to be 1. In other words, even if we modify the counter variable within a for loop, it will take on a predefined value at the next iteration. These predefined values are those numbers in range(n). They are defined once, and unless the for loop is terminated using a break, the counter j will be take on each value in range(n). Of course, if we really want to have j jump by two each time, we could do

In [12]:
for j in range(0, n, 2):
    print ("j = ", j)
    j += 2
j =  0
j =  2
j =  4

In practice, this “quirk” of Python for loops should not lead to problems, but it is important to understand (especially for those who plan to program in other languages like C++, for which changes made to the counter inside a for modify the loop execution).

Note. Changing the counter in a Python for loop does not modify the loop behavior.

Exercise: Try to define a while loop (in pseudocode or Python, or as a flowchart) that exhibits the same behavior as the “quirky” for loop above.

Nested for loops

Just like if statements can be nested, so, too, can for (and while) loops be nested. While nested if statements can always be written as (potentially much) more complicated, single statements, there are some tasks for which nested loops are truly required—but I have not proven that!

Exercise: Use a nested for loop to find the sum of the elements of \(5\times 5\) array of random numbers.

Solution:

In [13]:
n = 5
# The seed function just makes the random numbers the
# same every time so that these notes don't keep changing!
np.random.seed(1234)
# The rand function can produce single values or arrays.
# Note that its syntax for 2-D arrays does *not* use
# the double parentheses like np.ones and np.zeros
A = np.random.rand(n, n)
s = 0
for i in range(n):
    for j in range(n):
        s += A[i, j]
print('sum = ', s)
sum =  13.7180626162

Of course, we can use the built-in sum function (useful here to check our logic):

In [14]:
sum(sum(A))
Out[14]:
13.718062616246673

Exercise: Why do we need to use sum twice?

For the exercise above, each loop used a variable (i and j) that were independent. In other words, j was not dependent on i, and i did not dependend on j. This represents the simplest case of nested loops. Sometimes, though, an inner loop variable (here, j) depends on i. For example, consider the problem of computing the cumulative sum of an array \(a\). The cumulative sum of an array is another array of the same length whose \(i\)th element is defined as

\[c_i = \sum^i_{j=0} a_j \, .\]

For example, the cumulative sum of an array of three ones has the elements 1, 2, and 3.

Exercise: Use a for loop to compute the cumulative sum of an array of 5 random numbers.

Solution:

In [15]:
a = A[0] # steal the first row of the 2-D array above
c = 0*a  # easy way to initialize array of same size
for i in range(n):
    for j in range(i+1):
        c[i] += a[j]
print(a)
print(c)
[ 0.19151945  0.62210877  0.43772774  0.78535858  0.77997581]
[ 0.19151945  0.81362822  1.25135596  2.03671454  2.81669035]

The last element of c ought to be the sum of a:

In [16]:
c[-1]==sum(a)
Out[16]:
True

Using loops effectively requires practice, and nested loops in particular deserve ample attention. Here are some additional exercises to tackle:

Exercise: Starting with A = np.zeros((5, 5)), use two loops to produce

array([[ 1,  2,  3,  4,  5],
       [ 6,  7,  8,  9, 10],
       [11, 12, 13, 14, 15],
       [16, 17, 18, 19, 20],
       [21, 22, 23, 24, 25]])

Exercise: Starting with A = np.zeros((5, 5)), use two loops to produce

array([[ 1,  2,  3,  4,  5],
       [ 0,  7,  8,  9, 10],
       [ 0,  0, 13, 14, 15],
       [ 0,  0,  0, 19, 20],
       [ 0,  0,  0,  0, 25]])

Exercise: Starting with A = np.zeros((5, 5)), use two loops to produce

array([[ 1,  2,  3,  4,  5],
       [ 0,  6,  7,  8,  9],
       [ 0,  0, 10, 11, 12],
       [ 0,  0,  0, 13, 14],
       [ 0,  0,  0,  0, 15]])

Exercise: Use loops to compute the cumulative product of a = np.array([2,4,7,3,9]).

One Last Tidbit: continue

There are some occasions where, once something is done within a loop, you want to move right to the next iteration. For example, suppose we want to sum and print all the even integers from 1 through \(n\). An obvious solution is

In [17]:
n = 10
s = 0
for i in range(1, n+1):
    if not i % 2:
        s += i
        print(i)
print('s = ', s)
2
4
6
8
10
s =  30

However, the continue statement let’s us rewrite the program slightly as

In [18]:
n = 10
s = 0
for i in range(1, n+1):
    if i % 2:
        continue
    s += i
    print(i)
print('s = ', s)
2
4
6
8
10
s =  30

In some cases, use of continue may be simpler than if statements alone. Personally, I rarely use them. Note that continue (and, for that matter, break) applies only to the nearest for or while containing it. For example:

In [19]:
for i in range(5):
    print('i=', i)
    for j in range(5):
        if (j + i) % 2:
            continue
        elif i == 2:
            break
        print('  j=', j)
i= 0
  j= 0
  j= 2
  j= 4
i= 1
  j= 1
  j= 3
i= 2
i= 3
  j= 1
  j= 3
i= 4
  j= 0
  j= 2
  j= 4

Exercise: Load the previous example in Spyder and trace it using the graphical debugger. Try to guess whether or not the j will be printed for any possible values of i and j.

Further Reading

None at this time.