Loops in Python¶
Overview, Objectives, and Key Terms¶
In Lecture 5, the basics of programming
logic were introduced, including the idea of iteration. The emphasis
here and in Lecture 9 is on the logic of
iteration and its implementation via while
and for
loops in
Python. To proceed, one must be comfortable with the theoretical
coverage of iteration presented in Lecture
5.
Objectives¶
By the end of this lesson, you should be able to
- Use a
while
loop to solve simple problems using iteration - Turn pseudocode and flowcharts into Python code
- Use the graphical debugger in Spyder to trace and debug a program with iteration
Key Terms¶
while
break
- summation problem (as a common loop example)
- numerical convergence (as another common loop example)
- convergence criterion
A Motivating Problem¶
To jumpstart the lesson, let’s first look back to the example from
Lecture 5 in which the elements of an array
a
are printed. The algorithm, in pseudocode, for solving that task
is repeated here:
'''Algorithm to print out the elements of an array'''
Input a and n # where n is the length of array a
Set i to 0
While i < n
Print a[i]
Set i = i + 1
We can do that in Python using a nearly identical syntax:
In [1]:
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
1
1
2
3
5
8
13
Just like the if
statement, the while
statement requires the
pesky :
after the condition. Moreover, all the things to be done at
each iteration must be underneath and indented to the right of the
while
.
Here’s a slightly different problem, and one that serves as a template for many other problems: add the integers from 1 through n. An algorithm for solving the problem is shown below.
This flowchart is very similar to the one we saw earlier for printing
the element. The only real addition is the new variable s
, short for
sum.
Exercise: Explain why usings
is better than usingsum
for representing our integer sum.
We can adapt this flowchart directly to Python:
In [2]:
n = 10 # or we could use input
i = 1
s = 0
while i <= n:
s = s + i
i = i + 1
print(s)
55
Is that right? A useful formula for the same sum is \(s = \frac{(n+1)n}{2}\), and substitution of \(n = 10\) does yield \(s = 55\).
Iteration is challenging, for sure. Here are some simple exercises to tackle based on the example above for which you should be able to compute the expected value readily with pen and paper:
Exercise: Write a short program using a
while
loop to compute the sum of the integers from \(m\) through \(n\). What is the analytical expression for that sum?Exercise: Write a short program using a
while
loop to compute the product of the integers from 1 through \(n\). (What is that product called?)Exercise: Write a short program using a
while
loop to compute \(a^n\) given a number (int
orfloat
)a
and an integern
.
The break
Clause¶
So far, iteration has been limited to cases in which a fixed number of
iterations is executed, usually with a condition like
counter < max_counter
. Sometimes, iteration should cease based on
criteria that can be satisfied at any value of counter
and for
reasons unrelated to counter
.
As a simple example, suppose we modify the integer summation problem and
look instead for the largest value of n
that leads to a sum s
below some maximum value s_max
. You might be tempted to do the
following:
In [3]:
s_max = 100
s = 0
i = 0
while s < s_max:
i = i + 1
s = s + i
print("i = ", i)
print("s = ", s)
i = 14
s = 105
That’s close, but not quite right: the final value of s
is greater
then the maximum value of 100. The problem here is that the loop has to
repeat until it has produced an s
that exceeds the maximum. Thus,
in order to terminate, we must already have a value for s
greater
than we want. A simple fix is a post correction, e.g.,
In [4]:
s_max = 100
s = 0
i = 0
while s < s_max:
i = i + 1
s = s + i
s = s - i # subtract off the last amount added
i = i - 1 # and adjust i accordingly
print("i = ", i)
print("s = ", s)
i = 13
s = 91
However, such corrections are not always easy to do. Instead, we can
reorganize the while
so that it terminates from within the loop. It
all starts with the infinite loop:
while True:
print("this line will be displayed over and over again!")
This loop will never end unless you terminate execution from outside the
interpreter. In Spyder, that’s done by pressing the little red square to
the upper right of the console. It runs forever because the condition is
while True
and nothing can ever turn True
into False
. What
we need is some condition within the loop that leads to termination of
the iteration. Such conditions can be implemented using the break
clause:
In [5]:
while True:
print("this line will now be displayed just once!")
break
this line will now be displayed just once!
A break
statement may be used anywhere within a while
loop.
Here, break
is executed after just the first iteration. If, instead,
we wanted to print the message twice, we could do
In [6]:
counter = 0
while True:
print("this line will now be displayed just once!")
counter += 1
if counter == 2:
break
this line will now be displayed just once!
this line will now be displayed just once!
Note that an if
statement has been inserted within the while
statement. Just like if
statements can be nested in other if
statements, so to can they be nested inside while
(and for
, and
other constructs).
By using the break
statement, we can reorganize our program for
finding the
In [7]:
s_max = 100
s = 0
i = 0
while True:
if s + i + 1 > s_max:
break
i = i + 1
s = s + i
print("i = ", i)
print("s = ", s)
i = 13
s = 91
This output is identical to the previous version, and no post-corrections are needed.
Exercise: Run this program in Spyder and use the graphical debugger to trace its execution.
Numerical Convergence¶
The integer summation problem posed above is one of the canonical examples to use iteration, and it serves as a template for many other tasks. An equally important example is that of numerical convergence.
A very common goal in numerical computing is to produce a sequence of numbers that converges (in the limit of many terms) to some value of interest or, more often, as close to that value of interest as is desired. For example, Taylor series (and similar series) expansions can be used to evaluate the value of mathematical functions near a point of interest. The Taylor series expansion of \(\frac{1}{1-x}\) about \(x=0\) is given by
and for very small \(x\) (\(|x| \ll 1\)), we’d expect just the first few terms of the series to be suffient for approximating \(y(x)\) because \(x^i\) is smaller and smaller for increasing \(i\). The questions, though, are (1) how good is good enough? and (2) when do we know to stop?
The answer to the first question depends on the problem and what sort of information is provided. If the goal is to approximate some quantity, then very rarely is the exact value of that quantity known (otherwise, why would we need the approximation?). For such cases, we can only estimate how good an approximation is.
Exercise: Let \(y(x) = 1/(1-x)\). Approximate \(y(0.1)\) by using the series above, and terminate the iteration when the magnitude of the next term to be added is less than or equal to \(\tau = 10^{-4}\). Print out the approximate value for \(y\), the true value of \(y\), and the number of iterations performed.
Solution:
In [8]:
x = -0.1
y = 1/(1-x) # reference (exact) y
ya = 0 # approximate y
i = 0 # iteration counter
while abs(x**i) > 1e-4:
ya += x**i
i += 1
print('approximate value ', ya)
print(' reference value ', y)
print(' # of iterations ', i)
approximate value 0.9091
reference value 0.9090909090909091
# of iterations 5
The trick here is to iterate only as long as the next term to be added \(x^i\) is greater then the tolerance \(\tau\). Here, that threshold is \(10^{-4}\). Although the sum of all the terms we’re neglecting could be larger than the tolerance, often the series converges rapidly enough for such a criterion to work well in practice.
Note: A common termination criterion is \(|y_a - y^{old}_a| < \tau\), where \(y_a\) is the current approximation to \(y\), \(y_a^{old}\) is the previous value of \(y_a\), and \(\tau\) is the tolerance.
Sometimes, it’s not easy to pick a suitable tolerance. For some values of \(x\), the program above requires 1000’s of iterations, way more than the 5 observed for \(x=-0.1\). An useful alternative (or additional) termination criterion is to set a maximum number of iterations. By using both, one can provide a program that will produce a solution that meets the tolerance for any case not exceeding the maximum number of iterations allowed.
Note: It is good practice to include a limit on the number of iterations in addition to a tolerance on the approximation.
Based on this discussion, try tackling the following:
Exercise: Explain why we would not want to use
while x**i > 1e-4
.Exercise: Modify this program to compute no more than 20 terms of the series. For
x = 0.99
, which condition causes the loop to terminate? What is the final value ofya
?Exercise: Modify this program to use the absolute error
abs(y-ya)
. Do the results differ? By how much?
This program for approximating \(1/(1+x)\) is a great one for understanding how iteration can be used to solve numerical problems. The same pattern (i.e., keep iterating until the approximation looks good enough) shows up all over the place in the solution of both linear systems (i.e., \(\mathbf{Ax}=\mathbf{b}\)) and nonlinear systems (e.g., finding the roots of \(f(x)\)).
Further Reading¶
None at this time.