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:

  1. Do the steps in the loop

  2. Decide whether to keep going

  3. Do the steps in the loop

  4. Decide whether to keep going

  5. Do the steps in the loop

  6. 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:

  1. Decide whether to keep going, due to condition 1

  2. Do part A

  3. Decide whether to keep going, due to condition 2

  4. Do part B

  5. Decide whether to keep going, due to condition 1

  6. Do part A

  7. Decide whether to keep going, due to condition 2

  8. 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

  1. A predetermined maximum allowed number of iterations has been done

  2. 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:

  1. The approximation is sufficiently accurate — we use the criterion that \(|f(x_n)|\) is “small enough”.

  2. We are about to divide by zero (because \(Df(x_n) = 0\), and so have to give up.

  3. 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:

  1. Error estimate small enough: \(|x_n - x_{n-1}| \leq errorTolerance\).

  2. Avoiding division by zero: \(Df(x_n) = 0\).

  3. 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

  1. \(\sin(x) = 1/2\),

  2. \(x = \cos x\)

  3. one of the polynomials used in previous sections that has known real roots, and

  4. 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.