This is the solution to Homework 6: Problems - Python modules, loops, and I/O.

The following figure illustrates the grade distribution for this homework.

This homework aims at giving you some experience with Python for-loops and while-loops as well as reading user input from the Bash command line.

1.  The while-loop implementation of a for-loop. Consider the following example code, which converts a list of temperature values from Celsius to Fahrenheit, using a for-loop and then prints them on screen.

Cdegrees = [-20, -15, -10, -5, 0, 5, 10, 15, 20, 25, 30, 35, 40]
print ('    C     F')
for C in Cdegrees:
F = (9.0/5)*C + 32
print ('%5d %5.1f' % (C, F))

    C     F
-20  -4.0
-15   5.0
-10  14.0
-5  23.0
0  32.0
5  41.0
10  50.0
15  59.0
20  68.0
25  77.0
30  86.0
35  95.0
40 104.0


Write a while-loop implementation of the above code.

Cdegrees = [-20, -15, -10, -5, 0, 5, 10, 15, 20, 25, 30, 35, 40]
index = 0
print ('    C     F')
while index < len(Cdegrees):
C = Cdegrees[index]
F = (9.0/5)*C + 32
print('%5d %5.1f' % (C, F))
index += 1

    C     F
-20  -4.0
-15   5.0
-10  14.0
-5  23.0
0  32.0
5  41.0
10  50.0
15  59.0
20  68.0
25  77.0
30  86.0
35  95.0
40 104.0


2.  Consider the following nested list,

the following nested list:
q = [['a', 'b', 'c'], ['d', 'e', 'f'], ['g', 'h']]


Write a for-loop that extracts all the letters in the list and finally prints them all as a single string,

abcdefgh


q = [['a', 'b', 'c'], ['d', 'e', 'f'], ['g', 'h']]
s = ''
for i in q:
for j in range(len(i)):
s = s + i[j]
print(s)

abcdefgh


3.  Consider the following program,

from math import sqrt
for n in range(1, 60):
r_org = 2.0
r = r_org
for i in range(n):
r = sqrt(r)
for i in range(n):
r = r ** 2
print ('With {} times sqrt and then {} times **2, the number {} becomes: {:.16f}'.format(n,n,r_org,r))


Explain what this code does. Then run the code, and explain why do you the behavior observed. In particular, why do you not recover the original value $2$ after many repetitions of the same forward and reverse task?

This code will yield the following output:

from math import sqrt
for n in range(1, 60):
r_org = 2.0
r = r_org
for i in range(n):
r = sqrt(r)
for i in range(n):
r = r ** 2
print ('With {} times sqrt and then {} times **2, the number {} becomes: {:.16f}'.format(n,n,r_org,r))

With 1 times sqrt and then 1 times **2, the number 2.0 becomes: 2.0000000000000004
With 2 times sqrt and then 2 times **2, the number 2.0 becomes: 1.9999999999999996
With 3 times sqrt and then 3 times **2, the number 2.0 becomes: 1.9999999999999996
With 4 times sqrt and then 4 times **2, the number 2.0 becomes: 1.9999999999999964
With 5 times sqrt and then 5 times **2, the number 2.0 becomes: 1.9999999999999964
With 6 times sqrt and then 6 times **2, the number 2.0 becomes: 1.9999999999999964
With 7 times sqrt and then 7 times **2, the number 2.0 becomes: 1.9999999999999714
With 8 times sqrt and then 8 times **2, the number 2.0 becomes: 2.0000000000000235
With 9 times sqrt and then 9 times **2, the number 2.0 becomes: 2.0000000000000235
With 10 times sqrt and then 10 times **2, the number 2.0 becomes: 2.0000000000000235
With 11 times sqrt and then 11 times **2, the number 2.0 becomes: 2.0000000000000235
With 12 times sqrt and then 12 times **2, the number 2.0 becomes: 1.9999999999991336
With 13 times sqrt and then 13 times **2, the number 2.0 becomes: 1.9999999999973292
With 14 times sqrt and then 14 times **2, the number 2.0 becomes: 1.9999999999973292
With 15 times sqrt and then 15 times **2, the number 2.0 becomes: 1.9999999999973292
With 16 times sqrt and then 16 times **2, the number 2.0 becomes: 2.0000000000117746
With 17 times sqrt and then 17 times **2, the number 2.0 becomes: 2.0000000000408580
With 18 times sqrt and then 18 times **2, the number 2.0 becomes: 2.0000000000408580
With 19 times sqrt and then 19 times **2, the number 2.0 becomes: 2.0000000001573586
With 20 times sqrt and then 20 times **2, the number 2.0 becomes: 2.0000000001573586
With 21 times sqrt and then 21 times **2, the number 2.0 becomes: 2.0000000001573586
With 22 times sqrt and then 22 times **2, the number 2.0 becomes: 2.0000000010885857
With 23 times sqrt and then 23 times **2, the number 2.0 becomes: 2.0000000029511749
With 24 times sqrt and then 24 times **2, the number 2.0 becomes: 2.0000000066771721
With 25 times sqrt and then 25 times **2, the number 2.0 becomes: 2.0000000066771721
With 26 times sqrt and then 26 times **2, the number 2.0 becomes: 1.9999999917775542
With 27 times sqrt and then 27 times **2, the number 2.0 becomes: 1.9999999917775542
With 28 times sqrt and then 28 times **2, the number 2.0 becomes: 1.9999999917775542
With 29 times sqrt and then 29 times **2, the number 2.0 becomes: 1.9999999917775542
With 30 times sqrt and then 30 times **2, the number 2.0 becomes: 1.9999999917775542
With 31 times sqrt and then 31 times **2, the number 2.0 becomes: 1.9999999917775542
With 32 times sqrt and then 32 times **2, the number 2.0 becomes: 1.9999990380770896
With 33 times sqrt and then 33 times **2, the number 2.0 becomes: 1.9999971307544144
With 34 times sqrt and then 34 times **2, the number 2.0 becomes: 1.9999971307544144
With 35 times sqrt and then 35 times **2, the number 2.0 becomes: 1.9999971307544144
With 36 times sqrt and then 36 times **2, the number 2.0 becomes: 1.9999971307544144
With 37 times sqrt and then 37 times **2, the number 2.0 becomes: 1.9999971307544144
With 38 times sqrt and then 38 times **2, the number 2.0 becomes: 1.9999360966436217
With 39 times sqrt and then 39 times **2, the number 2.0 becomes: 1.9999360966436217
With 40 times sqrt and then 40 times **2, the number 2.0 becomes: 1.9999360966436217
With 41 times sqrt and then 41 times **2, the number 2.0 becomes: 1.9994478907329654
With 42 times sqrt and then 42 times **2, the number 2.0 becomes: 1.9984718365144798
With 43 times sqrt and then 43 times **2, the number 2.0 becomes: 1.9965211562778555
With 44 times sqrt and then 44 times **2, the number 2.0 becomes: 1.9965211562778555
With 45 times sqrt and then 45 times **2, the number 2.0 becomes: 1.9887374575497223
With 46 times sqrt and then 46 times **2, the number 2.0 becomes: 1.9887374575497223
With 47 times sqrt and then 47 times **2, the number 2.0 becomes: 1.9887374575497223
With 48 times sqrt and then 48 times **2, the number 2.0 becomes: 1.9887374575497223
With 49 times sqrt and then 49 times **2, the number 2.0 becomes: 1.8682459487159784
With 50 times sqrt and then 50 times **2, the number 2.0 becomes: 1.6487212645509468
With 51 times sqrt and then 51 times **2, the number 2.0 becomes: 1.6487212645509468
With 52 times sqrt and then 52 times **2, the number 2.0 becomes: 1.0000000000000000
With 53 times sqrt and then 53 times **2, the number 2.0 becomes: 1.0000000000000000
With 54 times sqrt and then 54 times **2, the number 2.0 becomes: 1.0000000000000000
With 55 times sqrt and then 55 times **2, the number 2.0 becomes: 1.0000000000000000
With 56 times sqrt and then 56 times **2, the number 2.0 becomes: 1.0000000000000000
With 57 times sqrt and then 57 times **2, the number 2.0 becomes: 1.0000000000000000
With 58 times sqrt and then 58 times **2, the number 2.0 becomes: 1.0000000000000000
With 59 times sqrt and then 59 times **2, the number 2.0 becomes: 1.0000000000000000


What is happening is that, 1 is returned for n >= 52 as square root of 2, that is, after 52 times square-root operation, the degree of accuracy required for representing the result goes beyond the degree of accuracy available in a Python float. Consequently, the later squaring operation on 1.00000000000000 will leave the number unchanged and therefore, 2 is not recovered.

4.  Consider the following code,

eps = 1.0
while 1.0 != 1.0 + eps:
print ('...............', eps)
eps /= 2.0
print ('final eps:', eps)


Explain what the code is doing. Run the code and observe the output. How could 1.0 != 1.0 + eps be False?

Here is the output of the code,

............... 1.0
............... 0.5
............... 0.25
............... 0.125
............... 0.0625
............... 0.03125
............... 0.015625
............... 0.0078125
............... 0.00390625
............... 0.001953125
............... 0.0009765625
............... 0.00048828125
............... 0.000244140625
............... 0.0001220703125
............... 6.103515625e-05
............... 3.0517578125e-05
............... 1.52587890625e-05
............... 7.62939453125e-06
............... 3.814697265625e-06
............... 1.9073486328125e-06
............... 9.5367431640625e-07
............... 4.76837158203125e-07
............... 2.384185791015625e-07
............... 1.1920928955078125e-07
............... 5.960464477539063e-08
............... 2.9802322387695312e-08
............... 1.4901161193847656e-08
............... 7.450580596923828e-09
............... 3.725290298461914e-09
............... 1.862645149230957e-09
............... 9.313225746154785e-10
............... 4.656612873077393e-10
............... 2.3283064365386963e-10
............... 1.1641532182693481e-10
............... 5.820766091346741e-11
............... 2.9103830456733704e-11
............... 1.4551915228366852e-11
............... 7.275957614183426e-12
............... 3.637978807091713e-12
............... 1.8189894035458565e-12
............... 9.094947017729282e-13
............... 4.547473508864641e-13
............... 2.2737367544323206e-13
............... 1.1368683772161603e-13
............... 5.684341886080802e-14
............... 2.842170943040401e-14
............... 1.4210854715202004e-14
............... 7.105427357601002e-15
............... 3.552713678800501e-15
............... 1.7763568394002505e-15
............... 8.881784197001252e-16
............... 4.440892098500626e-16
............... 2.220446049250313e-16
final eps: 1.1102230246251565e-16


What is happening is that after a certain number of divisions performed on the value of eps, the value goes beyond the highest float precision representatble by Python standard ($0.0000000000000001$), and therefore the value of eps is eventually rounded to exact zero. The nonzero eps value computed above is called machine epsilon or machine zero and is an important parameter to know, since it can lead to disasters in your very important complex calculations.

5.  Consider the following list,

numbers = list(range(10))
print(numbers)

[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]


Now run the following code, given the above list. Explain the weird behavior that you observe.

for n in numbers:
i = len(numbers)//2
del numbers[i]
print ('n={}, del {}'.format(n,i), numbers)


numbers = list(range(10))
for n in numbers:
i = len(numbers)//2
del numbers[i]
print ('n={}, del {}'.format(n,i), numbers)

n=0, del 5 [0, 1, 2, 3, 4, 6, 7, 8, 9]
n=1, del 4 [0, 1, 2, 3, 6, 7, 8, 9]
n=2, del 4 [0, 1, 2, 3, 7, 8, 9]
n=3, del 3 [0, 1, 2, 7, 8, 9]
n=8, del 3 [0, 1, 2, 8, 9]


What is really happening is that the list over which we are looping changes its content because of the modifications during on the list in the for-loop. The message in this exercise is to never modify a list that you are looping over. Modification is indeed technically possible, as shown above, but you really need to know what you are doing. Otherwise you will experience very strange program behavior.

6.  Consider a problem similar to what we had in the midterm exam: Write a Python function that when executed, asks the user to enter an integer number, then the function gives out the number of prime numbers that are smaller than the input integer number. Here is the answer to this question using only the knowledge of recursive functions and if-blocks,

def is_prime(n):

is_prime = True

def is_divisible(n,divisor):
if n<(divisor-1)*divisor: return False
if n%divisor==0: return True
else:
divisor += 1
return is_divisible(n,divisor)

if is_divisible(n,divisor=2): is_prime=False
return is_prime

def get_primes(n):
count = 0
if n == 1:
return count
else:
if is_prime(n):
count = 1
n -= 1
return count + get_primes(n)


get_primes(13)

5


(A) Now rewrite get_primes(n) and the other functions in the above code using for-loop this time. Name the new functions get_prime_for(n) and is_prime_for(n), with for in the names indicating that the functions now use for-loops.

def is_prime_for(x):
if x > 1:
n = x // 2
for i in range(2, n + 1):
if x % i == 0:
return False
return True
else:
return False

def get_primes_for(n):
count = 0
for i in range(2,n):
if is_prime(i):
count += 1
return count


Here is a test,

get_primes_for(13)

5


(B) Now compare the performance of the two functions get_primes(n=500) and get_primes_for(n500) using Jupyter’s or IPython’s %timeit magic function. Which one is faster?

%timeit get_primes(500)

1000 loops, best of 3: 1.32 ms per loop

%timeit get_primes_for(500)

1000 loops, best of 3: 1.69 ms per loop


Interesting, recursive functions seem to be faster than Python for-loops!