CMSC 14100 — Lecture 5

Loops

Looping statements allow us to repeat execution of parts of our program based on some condition. Most programming languages have two main flavours of loops, for and while loops, and this is the case for Python.

for loops

Suppose we want to do something simple like sum a few values. This is something we can do quite easily. However, in order to do so, we need to know how many values we are adding. But what if we don't know this? What if the number of values we want to sum up can change?

Up until now, we've only encountered simple, fixed-size data. However, we can represent multiple pieces of data in a sequence, by placing a series of values in square brackets.


    [2,4,3,4,3,1,5]
    

The number of items in such a list need not be fixed. In other words, if we are given an arbitrary list of items, we don't have any guarantee about how many items are in the list. It can contain five or five hundred items, but we will still need to handle them all the same. What we need is a way to execute statements based on the items in the sequence.

for loops are based on looping over items in a given sequence, executing a series of statements for each item. The form of a for loop is


    for <loop_variable> in <sequence>:
        <statements>
    

Just like conditional statements, the statements to be executed in the loop need to be appropriately indented.

One can read this as "for each item in the given sequence, do the following".


    nums = [2,4,3,4,3,1,5]
    for n in nums:
        print(n)
    

How does this work? Each item of the list is assigned to the loop variable n, in the order that the items appear in the list. This allows us to "do something" with each item in the list. So for each item in the list, the statements in the body of the loop are executed. A single execution of the loop body is an iteration of the loop. Once we have gone through the entire list of items, the loop ends.

Let's walk through this in more detail for the above for loop.

  1. The loop variable n is assigned to the first value of the list, 2.
  2. The loop body, print(n) is executed.
  3. The above two steps are repeated with the next item in the list (4, then 3, and so on...)

This seems straightforward enough, but I will mention something briefly for you to think about: lists can be empty (denoted by []), so what happens if we try to loop over an empty list?

Going back to our original intent, we would like to use this to sum the items in the list. Roughly speaking, we are taking the items of our list and accumulating them into a single value.


    nums = [2,4,3,4,3,1,5]
    total = 0
    for n in nums:
        total = total + n
    print(total)
    

There are a few things to notice about this loop, because it represents a very common pattern among loops that aggregate values.

First, notice that in order to accumulate our value, we need a way to store information in between each iteration. We use a variable to do this (in this case, total) which we call the accumulator. Observe that the accumulator gets reassigned with every iteration of the loop. Because the accumulator changes so frequently, it's important to be careful when reasoning about it and making sure to understand how it changes with each iteration.

An interesting question is where the accumulator should be initialized. One common error is initializing the accumulator inside the body of the loop. If we're careful in tracing our code, it's easy to see why this is a problem: placing this inside the loop means that the variable gets initialized with each iteration.

Another common error is forgetting to initialize the accumulator in the first place. One can see how omitting this will lead to issues in the first iteration: total = total + n will attempt to evaluate the variable total when performing the assignment, but it doesn't exist yet!

This discussion of initializing the accumulator brings us back to an earlier question: what happens when we try to run this loop on an empty list? What should we expect should happen?

Now, recall that in both conditional statements and loops, the body of these statements can be a series of any other statements. This means that we can use conditional statements inside the body of a loop or a loop in the branches of a conditional.


    nums = [2,4,3,4,3,1,5]
    for n in nums:
        if n % 2 == 0:
            print(n, "is even")
        else:
            print(n, "is odd")
    

Notice here that indentation is important for delimiting which blocks belong to which statements.

Now, a common action we would like to perform is the ability to count. Our discussion of for loops so far suggests the following.


    nums = [0,1,2,3,4,5,6]
    for n in nums:
        if n % 2 == 0:
            print(n, "is even")
        else:
            print(n, "is odd")
    

This is a bit silly and it seems like we should be able to avoid having to type 100 numbers if we just wanted to count to 100. Python has a mechanism for this, the range function.


    for n in range(6):
        if n % 2 == 0:
            print(n, "is even")
        else:
            print(n, "is odd")
    

The range function can take one or two inputs. If range(n) is specified, then it produces the numbers $0, 1, \dots, n-1$. This is important to keep in mind: range(n) begins with $0$ and will not produce $n$! One way to think about this is that it produces $n$ integers, starting at $0$.

This if range(m,n) is specified, then the numbers $m, m+1, \dots, n-1$ are produced. Notice that while $m$, the lower bound, is produced, the upper bound $n$ is not.

Suppose that we want to test whether a number is prime (in truth, much more complicated to do efficiently than it appears). This turns out also be an aggregation problem. Why? Let's examine the code.


    n = 7
    is_prime = True
    for i in range(2,n):
        if n % i == 0:
            is_prime = False
    

Notice that this has the form of an accumulation: we initialize the accumulator is_prime and our code is really trying to accomplish computing the equivalent of this logical expression: \[n \not\equiv 0 \bmod 2 \wedge n \not\equiv 0 \bmod 3 \wedge \cdots \wedge n \not\equiv 0 \bmod{n-1}.\] The Python equivalent of this expression is


    is_prime = (n % 2 != 0) and (n % 3 != 0) and ... and (n % n-1 != 0)
    

So a more literal accumulation might replace the if statement with


    is_prime = is_prime and (n % i != 0)