Nov. 7, 2020

# For loop and control statements

Control structure to cycle over iterables using for loops. Iterate over integer and high precision number sequences and string sequences. Control loop using break, continue, and else statements.

## FOR loop for integers

The basic for loop cycles through Python iterables like lists. Provided a list of integers, the loop acts like a foreach operation, picking items serially from start to finish. A range can be used to generate the sequence of numbers instead of an explicit list.

Py3: For loop behaves more like foreach

``````#   loop over a list of integers
nums = [1,2,3,4,0]
for n in nums:
print(n, end='; ')
#= 1; 2; 3; 4; 0;

#   loop using using range
for n in range(-1,6):
print(n, end='; ')
#= -1; 0; 1; 2; 3; 4; 5;
``````

The iterable can be any sequence of numbers, which can be generated by a function.

Py3: Custom sequence of integers

``````#   custom sequence using formula (n-3)(n-2)
nums = [1,2,3,4,5]
for n in nums:
seq = (n-3)*(n-2)
print(n, end='; ')
#= 2; 0; 0; 2; 6;
``````

Instead of storing a large list of numbers, a generator function can be used to return single elements of a sequence on demand, consuming very little memory.

Py3: Custom sequence generator

``````#   instead of storing huge lists, a generator
#   can produce numbers on demand
def intgen(start):
x = start
while x <=5:
n = (x-3)*(x-2)
yield n
x = x + 1

for n in intgen(1):
print(n, end='; ')
#= -2; 0; 0; 2; 6;
``````

Notes: Cycling over items using for

The for loop in Python can be better termed as foreach loop that cycles through items from an iterable. When the iterable is a list, the members are explicitly listed. It is very useful feature, as the list can be crafted to have any sequence of numbers one requires.

If the list of numbers is too long to explicitly write out, then it can be created using a function like range which can create simple linear sequences. For non-linear sequences, a formula can translate the linear sequence. There are no functions built in to generate floats, but it is simple to create it from integers.

If the lists are too long to store in memory, then a generator function can be crafted to yield items one at a time on demand. The generators can be embedded with same formula to generate lists. The logic inside the generators can be as complex as needed and can create Fibonacci sequence, or prime number sequence as some well-known examples.

## FOR loop over floats

For loop iteration over floating point numbers are similar to integers. Floats can also be created by transforming a sequence of integers algebraically.

Py3: Floating point numbers

``````#   list of custom floats
nums = [1.0, 1.3, 1.6, 1.9]
for n in nums:
print(n, end='; ')
#= 1.0; 1.3; 1.6; 1.9

#   floats from integer list
nums = [1, 2, 4, 5, 8, 10]
for n in nums:
print(1/n, end='; ')
#= 1.0; 0.5; 0.25; 0.2; 0.125; 0.1
``````

By the very definition of floating point numbers, the precision is limited. This is often forgotten, or taken for granted leading to troubling soft errors.

Py3: Float precision using counter

``````#   generate linear float sequence
#   floating point numbers are not fully precise
float_n = 2.2
for i in range(5):
print(float_n)
#   next float calculation
float_n += 0.2

#=  2.2
#   2.4000000000000004
#   2.6000000000000005
#   2.8000000000000007
#   3.000000000000001
``````

Py3: Float precision using translation formula

``````#   precision can vary by the way function is crafted
for n in range(5):
#   formula having floating point coefficients
float_n = 2.2 + n*0.2
print(float_n)

#=  2.2
#   2.4000000000000004
#   2.6
#   2.8000000000000003
#   3.0
``````

Py3: Float precision with integer coefficients

``````#   casting formulae using integers is a good idea
for n in range(5):
#   formula using integer coefficients
float_n = (22+n*2)/10
print(float_n)

#=  2.2
#   2.4
#   2.6
#   2.8
#   3.0
``````

Notes: Floating point sequences

Python provides the range function to create a sequence of integers. However there is no built in method to create a sequence of floats. So unlike other programming languages, it is not directly possible to do floating point iteration. It is however very simple to create a counter within a loop to generate floating point values. A translation formula can also convert the integers into floating point values. The formulae can be non-linear or periodic producing any floating point sequence needed.

The floating point precision is another important factor, and depends on the computer hardware and operating system. It is well-known fact that there are floating point inaccuracies due to the way the numbers are defined and stored. This can cause soft errors which can be troubling. Consider a conditional statement to halt execution for a value greater than 2.8. Due to floating point precision issues this may be triggered unintentionally by a value 2.800000003, whose original value was supposed to be 2.8. The small inaccuracy due to precision can cause the code to exit.

Using coefficients which are floating point numbers can cause accumulating precision errors. The use of integer coefficients in formulae can reduce the errors. Division by odd numbers like 3 or 7 can lead to approximation errors which will affect precision. Python provides high precision decimal class objects which can perform calculation to arbitrary precision. It takes compute power, but may be necessary for some financial or scientific scenarios.

## FOR loop using high precision numbers

When floating point precision is not enough, Python provides high precision decimals which can be used in for iterations.

Py3: Decimal precision

``````#   load decimal library
#   set to 3 decimal point precision
import decimal as dc
dc.getcontext().prec = 3

#   define decimal numbers
#   set up a counter using decimals
#   result is a series of accurate numbers
x = dc.Decimal('2.4')
for n in range(5):
print(x, end='  ')
x -= dc.Decimal('0.2')
#= 2.4  2.2  2.0  1.8  1.6
``````

High precision replacement for range.

Py3: Generator version and break statement

``````#   decimal generator is source of an infinite series
#   of high precision numbers with simple formula
#   n_new = n_old + d
#---------------------------------------------------
import decimal as dc
dc.getcontext().prec = 3

def decimal_gen(dstart, dstep):
n = dc.Decimal(dstart)
d = dc.Decimal(dstep)
while True:
yield n
n += d
#---------------------------------------------------

#   extract 5 values from decimal_gen with starting value 0.5
#   and step of -0.25
for d,_ in zip(decimal_gen('0.5', '-0.25'), range(5)):
print(d, end=' ')
#= 0.5 0.25 0.00 -0.25 -0.50

#   same idea as before, but use enumerate to stop after
#   5th number. The break statement helps exit loop
for e, d in enumerate(decimal_gen('0.5', '-0.25')):
print(d, end=' ')
if e >= 5:
break
#= 0.5 0.25 0.00 -0.25 -0.50
``````

Notes: High precision decimal generator

Floating point numbers should be sufficient for most scientific computing. When high tolerance over precision is needed, Python Decimal module can be used. Decimal numbers at set precision can be created and tracked during loops.

A simple decimal range generator can be created that generates infinite number of sequence values provided with an epoch and step. Since the generator is infinite, looping over without control it will cause an endless loop. This can be controlled either by the zip function which takes in multiple iterables, and stops when one ends. The range object in the zip function is just being used as a gatekeeper to stop when assigned number of iterations are completed. Enumerate function can also be used to keep count of iterations, and on reaching the threshold quantity, the loop can be halted using a break statement.

## FOR loop on text strings

Loops over text in form of a sentence can be at word or letter level. Both are easy to perform.

Py3: Text loops with letters

``````#   print selected letters from given text
txt = 'python loops'

#   txt is iterable at letter level
for letter in txt:
#   only letters in python are printed
#   so 'l', ' ', 's' are skipped over
if letter in 'python':
print(letter, end='-')
#= p-y-t-h-o-n-o-o-p-
``````

Py3: Funny case change, word level

``````#   cycle helps alternate between items
from itertools import cycle

#   list of case functions to apply
funcs = ['lower', 'upper']

#   string to operate on
txt = 'Loop over some text and change cases'

#   simultaneously loop over words, and
#   cycle alternately over functions
#   split breaks up sentence at white spaces into words
for word, fn in zip(txt.split(), cycle(funcs)):

#   change case attribute of each word
newtxt = getattr(word, fn)()
print(newtxt, end=' ')

#= loop OVER some TEXT and CHANGE cases

#   functions can also be called by name
#   funcs = [str.lower, str.upper]
#   newtxt = fn(word)
``````

Notes: Word or letter

Loops over text can take each letter from a string, and do something with each letter. The letter can be matched against a template for filtering, or it can be altered. For word level, the sequence has to be split into words using separator characters. Split function without any extra parameters will chop the sentence up into words at whitespaces.

Keeping track of alternate loops is achieved with cycle from itertools. It is very handy to have automatic tracking of which function to use from the list of functions. This allows to change the case of alternate words for the sentence. The functions can be called using getattribute method, or can be stored and called in functional form.

## Nested FOR loops

There can be multiple loops within loops to create nested structure.

Py3: Loop within loop - independent execution

``````#   nested independent loops
for y in ['a','b','c']:
for x in [1, 2, 3]:
print(f'{y}{x}  ', end='')
print('')
#=  a1 a2 a3
#   b1 b2 b3
#   c1 c2 c3
``````

Py3: Outer loop element modified by inner loop

``````#   variable replication
#   outer loop element modified by inner
for t in 'python'[1::2]:
for n in range(3):
print(n, t*n)
#=  0
#   1 y
#   2 yy
#   0
#   1 h
#   2 hh
#   0
#   1 n
#   2 nn
``````

Py3: Independent loops, tied by common condition

``````#   numbers between 2 and 100 divisible by 2, 3, 5
#   three loops agreeing on a value
last= 101
for d2 in range(2, last, 2):
for d3 in range(3, last, 3):
for d5 in range(5, last, 5):
if d2 == d3 == d5:
print(d2, end=' ')
#= 30 60 90
``````

Py3: Interacting nested loop elements

``````#   create 2 dimensional list
#   interacting loops
arr = []
for y in range(1,7,2):
row = []
for x in range(2,9,3):
row.append(x*y)
arr.append(row)
print(arr)
#= [
#   [2, 5, 8],
#   [6, 15, 24],
#   [10, 25, 40]
# ]
``````

Py3: Dependent nested loops

``````#   find highest value letter for every word
#   outer loop is over words, inner over letters
txt = 'find the highest letter for every word'
topletters = []

#   outer loop over words
for word in txt.split():
top = 'a'
#   inner loop over letters in each word
for letter in word:
top = letter if letter > top else top
topletters.append(top)
print(topletters)
#= ['n', 't', 't', 't', 'r', 'y', 'w']
``````

Notes: Nested loops

Nested loops can operate with complex datasets in multiple dimensions. The nested inner loops execute more often, than the outer loops. This is similar to the minutes and hour pointers of an analog clock. On completion of inner loops the other loop increments by one. The inner and outer loops can be independent, or can interact with each other.

## BREAK out of FOR loop

Break statement allows stopping execution of loops prematurely.

Py3: Stop early if match

``````#   match word to break or stop
txt = 'when loop finds break or stop, it quits'
for word in txt.split():
if word in ('break', 'stop'):
print(word)
print('Quitting!')
break
else:
print(word, end=' ')
#= when loop finds break
#  Quitting!
``````

Py3: Break on condition

``````#   stop when decimal part of sqrt < cube_rt
#   for numbers 2 to 9
#
for n in range(2, 10):
#   d2 is decimal part of square root
sq = n ** (1/2)
d2 = sq - int(sq)

#   d3 is decimal part of cube root
cu = n ** (1/3)
d3 = cu - int(cu)

#   show the decimal portions
print('{n} sq:{:.4f}  cu:{:.4f} '.format(n, sq, cu))
if d2 < d3:
print(f'Exiting @ n={n}')
break
#=  2 sq:1.4142  cu:1.2599
#   3 sq:1.7321  cu:1.4422
#   4 sq:2.0000  cu:1.5874
#   Exiting @ n=4
``````

Notes: Quitting a loop when condition is right

For loops stop when it runs out of items to iterate over. It is often needed to quit the loop prematurely, when certain conditions are met. Most often conditions are such that the reason for looping has been made immaterial, and to continue is a waste of compute. In other cases a termination signal is to prevent some catastrophe that can occur if iteration continues. For all these cases, it is possible to break the current loop and proceed with the next statement outside the loop using the break statement.

## Nested BREAK out

Breaking out of nested loops only works for the inner loop.

Py3: Stop early if match in nested loop

``````#   outer loop
for outer in [1,2,3]:
print(f'outer >> {outer}')

#   inner loop
for inner in [2,4]:
print(f'inner : {inner}')
if outer==inner:
print(f'>>break at {inner}')
break
#   on inner loop completion
print('Completed inner')
#   on outer loop completion
print('Completed outer')

#=  outer >> 1
#   inner : 2
#   inner : 4
#   Completed inner
#   outer >> 2
#   inner : 2
#   >>break at 2
#   Completed inner
#   outer >> 3
#   inner : 2
#   inner : 4
#   Completed inner
#   Completed outer

#   break inside inner loop is triggered at 2
#   however outer loop keeps running, and a new
#   inner loop is initiated
``````

Exiting nested loops when match is triggered.

Py3: Exit all loops

``````#   outer loop
exit_flag = False
for outer in [1,2,3]:
print(f'outer >> {outer}')

#   inner loop
for inner in [2,4]:
print(f'inner : {inner}')
if outer==inner:
print(f'>>break at {inner}')
exit_flag = True
if exit_flag:
break

print('Completed inner')
if exit_flag:
break
print('Completed outer')

#=  outer >> 1
#   inner : 2
#   inner : 4
#   Completed inner
#   outer >> 2
#   inner : 2
#   >>break at 2
#   Completed inner
#   Completed outer

#   break inside inner loop is triggered at 2
#   exit_flag is tracked at each loop to break out of it
``````

Notes: Breaking out of nested loops

For nested loops, a break statement within inner loop only disrupts the inner loop execution. The outer loop cycling is still operative, which can drive subsequent inner loops. In order to break out of all loops in a nested configuration, a tracking flag is needed. Once the flag is set within any level of the nested loop, each of the loops are halted by individual break statements.

If the code segment can be written as a function, then instead of a break statement, one can exit the function using a return statement.

## ELSE on loop completion

If entire cycle was a success, something special may need to happen under such a situation. The else statement is executed if the loop reached the last element without incidents.

Py3: Full cycle success

``````#   simple prime checker for 2 to 50
for n in range(2, 50):
k = int(n ** 0.5) + 1
for d in range(2, k):
#   if break occurs
#   then else is not executed
if n % d == 0:
break
else:
#   this only executes if
#   number 'n' is not divisible
#   for range 2 to sqrt(n)
#   which means number is prime
print(n, end=' ')
#= 2 3 5 7 11 13 17 19 23 29 31 37 41 43 47
``````

Py3: Success tracking with flags

``````#   same outcome can be replicated, with the
#   use of a status flag 'isdiv'
for n in range(2, 50):
k = int(n ** 0.5) + 1

#   isdiv is reset for each inner loop
isdiv = False
for d in range(2, k):
#   if divisible, isdiv is set True
if n % d == 0:
isdiv = True
break

#   isdiv remains False only if number
#   is prime
if not isdiv:
#   if not divisible
print(n, end=' ')
#= 2 3 5 7 11 13 17 19 23 29 31 37 41 43 47
``````

Notes: Checks for full iteration cycle success

Iteration with a for loop being done to check failure against multiple conditions means completing the full cycle is a sign of success. If there has been no break statement to stop iteration, and all the items to be iterated over have been exhausted, the loop enters a special condition using the else control keyword. This special state has to be tracked with independent logic flags in most programming languages. Python provides a built it method to enter this state and make some special actions, like declaring a number to be prime.

## Loop back and CONTINUE

Skipping to the next item in the iterable sequence using continue statement.

Py3: Continue to next item for numbers

``````#   skip over even numbers and sum the odds
tot = 0
for n in range(10):
#   check if even, then loop to next
if n % 2 == 0:
continue
#   this part is not visited
#   if number is even, so only
#   odd numbers are summed up
tot += n

#   show the numbers being summed
print(n, end=' ')
#   show the sum
print('--> ', tot)
#= 1 3 5 7 9 --> 25
``````

Py3: Filter using continue to remove all punctuations

``````#   text to operate on
txt = 'Hi, Python! Loop need a break, now.'

#   final cleaned text
cleaned = ''

#   iterate over letters
for letter in txt:
#   match if letter matches
#   if so loop back
if letter in ",.!:'":
continue
#   letter added to cleaned if
#   not in filter list
cleaned += letter
#   show cleaned text
print(cleaned)
#= Hi Python Loop need a break now
``````

Notes: Skipping to filter

When some item within the iterable sequence triggers a state which needs the loop to skip extra operations and go over to the next item, the continue statement is needed. It acts like a short-circuit, from further actions. Here the loop is not stopped, but the next available item is requested for processing. The continue statement is useful for filtering out unwanted items, or branching back and preventing additional operations.

## Summary of statements

The for loop has three control statements.

Method/Statement Property
else execute on normal completion
break premature exit from loop
continue loop to next element
decimal_gen() high precision version of range