Iteration refinements: break and continue
Contents
8. Iteration refinements: break
and continue
#
8.1. Introduction#
Altough all iterations an be programmed with either a for
or while
loop — and in fact as noted below,
all iteration can be done with while
loops — the two extra statements break
and continue
can sometimes make code more readable and concise.
For example, a break
statement can be used to avoid the repetition seen in
Examples B and D of Iteration with while.
8.2. Case 1: loops that must execute at least once#
It is common numerical computing that a loop must be execute at least once, because the information used to decides whether to keep going is only generated within the loop itself.
The structure is like this:
Do the steps in the loop
Decide whether to keep going
Do the steps in the loop
Decide whether to keep going
Do the steps in the loop
Decide whether to keep going
and so on
With a while
loop, we can do this:
Do the steps in the loop
while "we are not there yet":
Do the steps in the loop
The statement break
can avoid this repetition of code:
its meaning is that it stops the (while
or for
) loop immediately, with execution continuing at the next line of code afer the loop.
There is still one thing to deal with: making the loop run for sure the first time.
With a while
loop this is done by the slightly inelegant trick of making the while
statement always keep going,
relying soley on the break
to end things:
while True:
Do the steps in the loop
if "our work here is done":
break
(Note that the condition for the break
is the logical negation of the one used in the while
statement version.)
8.3. Case 2: loops with multiple stopping conditions, that can arise at different stages#
A second common pattern for iteration is when the information used to decide whether to stop iterating arises part way through the calculations in the loop — for example, a division by zero is about to happen, so you must bail out:
Decide whether to keep going, due to condition 1
Do part A
Decide whether to keep going, due to condition 2
Do part B
Decide whether to keep going, due to condition 1
Do part A
Decide whether to keep going, due to condition 2
Do part B
Etc.
This can be donw with a while
loop:
while "condition 1 to keep going":
part A
if "condition 1 to finish":
break
part B
8.4. Case 2b: loops with multiple stopping conditions, one suitable for a for
loop#
A second common pattern for iteration is when the information used to decide whether to stop iterating arises part way through the calculations in the loop — for example, a division by zero is about to happen, so you must bail out:
A common sub-case of the above — to be seen in the next example is where there are two reasons to stop
A predetermined maximum allowed number of iterations has been done
Something comes up during a pass of the loop which means you can (or must) end the iterations.
For example with an iterative calculation like that for the cube root in Example B of Iteration with while, it might not be certain in advance that sufficient accuracy will ever be achieved, so a maximum allowed number of iterations is needed to avoid a possibly unending repetition.
That example could be redone with such a limit, using afor
loop:
Example A: cube roots again#
a = 8
root = 1
# I will tolerate an error of this much:
errorTolerance = 1e-8
# and allow at most this mant iterations:
iterationsMax = 10
for iteration in range(iterationsMax):
root = (2*root + a/root**2)/3
errorEstimate = abs(root**3 - a)
if errorEstimate <= errorTolerance: # we are done
break
print(f'The cube root of {a:g} is approximately {root:20.16g}')
print(f'The backward error in this approximation is {abs(root**3 - a)}')
print(f'This required {iteration} iterations')
The cube root of 8 is approximately 2.000000000012062
The backward error in this approximation is 1.447428843448506e-10
This required 5 iterations
Note A side benefit of using for
loops in situations like this is getting an “automatic” counting of how much work was required,
by the crude measure of how many iterations were needed.
Example B: Solving equations by Newton’s method#
As you might have seen in a calculus course Newton’s Method for solving an equation \(f(x) = 0\) is based on getting a sequence of approximations \(x_0, x_1, x_2 , \dots\) with
Get an initial guess, \(x_0\)
Get the rest successively with \(x_{n+1} = x_n - f(x_n)/Df(x_n)\) Here I use the notation \(Df\) for the derivaitve \(f'\), because it can be the name of a Python function.
There are three reasons that the iteration should stop:
The approximation is sufficiently accurate — we use the criterion that \(|f(x_n)|\) is “small enough”.
We are about to divide by zero (because \(Df(x_n) = 0\), and so have to give up.
It is taking too many iterations, as in the previous example — this is a real hazard with Newton’s Method if you are not careful!
a = 8. # We seek its cube root, as the solution of f(x) = x^3-a = 0
def f(x): return x**3 - a
def Df(x): return 3*x**2
x = 1. # All x_n values will be stored in x
errorTolerance = 1e-15
iterationsMax = 10
print(f"Before the iterations, x_0 = {x}, with backward error {abs(f(x))}")
for iteration in range(iterationsMax):
Df_of_x = Df(x)
if Df_of_x == 0: # Give up
break
x -= f(x)/Df_of_x
errorEstimate = abs(f(x))
print(f"After {iteration+1} iterations, x_{iteration+1} = {x}, with backward error {errorEstimate}")
if errorEstimate <= errorTolerance: # Success
break
print("")
print(f"The solution is approximately {x}")
print(f'The backward error in this approximation is {abs(root**3 - a)}')
print(f'This required {iteration} iterations')
Before the iterations, x_0 = 1.0, with backward error 7.0
After 1 iterations, x_1 = 3.3333333333333335, with backward error 29.037037037037045
After 2 iterations, x_2 = 2.462222222222222, with backward error 6.92731645541838
After 3 iterations, x_3 = 2.081341247671579, with backward error 1.0163315496105625
After 4 iterations, x_4 = 2.003137499141287, with backward error 0.03770908398584538
After 5 iterations, x_5 = 2.000004911675504, with backward error 5.894025079733467e-05
After 6 iterations, x_6 = 2.0000000000120624, with backward error 1.447482134153688e-10
After 7 iterations, x_7 = 2.0, with backward error 0.0
The solution is approximately 2.0
The backward error in this approximation is 1.447428843448506e-10
This required 6 iterations
8.5. Skipping part of the code in a loop: the continue
statement#
Another situation that can arise, though less often in numeircla computing, is that you sometimes can omit the result of the steps in an iteration, but wish to continue with the rest — for example, the result of the iteration has been gone more easily than usual.
The continue
statement does this.
Example C#
For the natural numbers n
from 0 to N
decide of they are a multiple of 3, and if not, print the cube of the remainder are dividing by three:
N = 10
for n in range(N+1):
n_by_3 = n%3
if n_by_3 == 0:
continue
print(f"For {n=} the remainder cubed is {n_by_3**3}")
For n=1 the remainder cubed is 1
For n=2 the remainder cubed is 8
For n=4 the remainder cubed is 1
For n=5 the remainder cubed is 8
For n=7 the remainder cubed is 1
For n=8 the remainder cubed is 8
For n=10 the remainder cubed is 1
This effect can also be achieved with an if
statement deciding whether to execute the remaining code in the loop:
N = 10
for n in range(N+1):
n_by_3 = n%3
if n_by_3 != 0:
print(f"For {n=} the remainder cubed is {n_by_3**3}")
For n=1 the remainder cubed is 1
For n=2 the remainder cubed is 8
For n=4 the remainder cubed is 1
For n=5 the remainder cubed is 8
For n=7 the remainder cubed is 1
For n=8 the remainder cubed is 8
For n=10 the remainder cubed is 1
Thus the benefits are less obvious than with break
;
it can payoff when there are multiple places in the loop to continue
,
in which situation it can help avoid convoluted nesting of if
statements.
Execise A: Newton’s Method#
A more careful algorithm for Newtons’s methods uses an actual estimate of the error \(e_n = |r - x_n|\) where \(r\) is the root: \(f(r) = 0\).
This in turn is estimated by the difference between the two most recent approximations: \(e_n \approx E_n = |x_n - x_{n-1}|\). (This is typically quite pessimistic, with the error usually being far smaller, so the algorithm is “cautious”.)
One difficulty with this — and why it was not used in the example above — is that the estimate is not available till towards the end of the first iteration, so it is not well-suited to a while
loop.
Thus there again three reasons the iterations can end, all coming at different places in the loop:
Error estimate small enough: \(|x_n - x_{n-1}| \leq errorTolerance\).
Avoiding division by zero: \(Df(x_n) = 0\).
Too many iterations, suggesting an infinite loop:
iteration > maxIterations
.
The final issue to deal with is having both \(x_n\) and \(x_{n-1}\) available at the n-th iteration, preferably done without storing the whole sequence but just just the two most recent values.
Implement a Python function with usage
`(root, errorEstimate, iterationsNeeded) = newton(f, Df, x0, errorTolerance, iterationsMax)`
and test it on a few equations including
\(\sin(x) = 1/2\),
\(x = \cos x\)
one of the polynomials used in previous sections that has known real roots, and
one of the polynomials that has no real roots.
8.6. Footnote: every for
loop could be done as a while
loop, but …#
The for loop
for i in range(a,b,step):
do stuff
can be replaced by
i = a
while a < b:
do stuff
i += step
but this is less clear and concise. More generally iteration over a list or such can also be reworked this way, but it becomes even more convoluted.