Control Flows

There are two primary tools for control flow: choices and loops. Choices, like if statements allow you to run different code depending on the input. Loops, like for and while, allow you to repeatedly run code, typically with changing options

Conditionals

The basic form of a condition is the if statement.

If statement

In If control flow, if the condition is True then true_action is evaluated. If the condition is false, nothing happens.

if condition:
  true_action
x = -3
if x < 0:
  print(f'{x} is a Negative number')
-3 is a Negative number
x = 3
if x < 0:
  print(f'{x} is a Negative number')

If else statement

if condition:
    true_action
else:
    false_action

In if else statement, if the condition is True then true_action is evaluated. otherwise the false_action is evaluated.

x = 3
if x < 0:
  print(f'{x} is a Negative number') 
else:
  print(f'{x} is a positive number')
3 is a positive number

Sometimes you need to evaluate a lot of statements before you find the value you want.

x = 3
if x > 0:
  z = 3
  y = z * x
else:
  z = -0.03
  p = z * 10
  y = x * p
y
9

elif Statement.

In other langauges this is commonly known as else if statements

if condition_one:
    action_one
elif condition_two:
    action_two
else:
    false_action

In this case, we have various conditions. Only one of the conditions where the condition evaluates to True will be evaluated. If none of the conditions is True then the else statement is evaluated.

x = "plant"
if x == "cow"  or x == "horse" or x == "dog": #same as: x in ('cow','horse', 'dog')
  print(4, 'limbs')
elif x in ("human", "bird"):
  print(2, 'feet')
elif x == "plant":
  print(0, 'limbs')
else:
  print("Unknown input")
0 limbs

nested if else statements.

These are just if-else statements inside other if-else statements to provide better decision making

Note the indentation. The statements to be evaluated are indented to ensure that they are in the right choice operation.

The ternary operator

Sometimes the statements to be evaluated are very short. In that case, we can use a tertiary statement. This looks like shown below

true_action if condition else false action
x = 3
print('Negative') if x < 0 else print('positive')
positive
# get absolute of a number
y = -3
y = -y if y < 0 else y
y
3

This is useful in cases where we need to assign a value to a variable based on a simple condition, and we want to keep our code more concise — all in just one line of code. It’s particularly handy when you want to avoid writing multiple lines for a simple if-else situation.

Example

Suppose we had a function defined like below:

\[ y = \begin{cases} 1 - a& \text{if } a < -3\\ a &\text{if } -3\le a\le3\\ 2a & \text{ Otherwise }\end{cases} \]

We could write it using if else if statement:

def f(a):
  if a < -3:
    y = 1 - a
  elif -3 <= a <= 3:
    y = a
  else:
    y = 2 * a
  return y

f(-6)
7
f(-3)
-3
f(0)
0
f(10)
20

Take note that the function name is different from the variable within the function. We have to ensure this is the case in order to disambiguate the function from the variable.

At the same time, we could use the ternary operator:

def y(a):
  return 1 - a if a < -3 else a if -3 <= a <= 3 else 2*a

y(-6)
7
y(-3)
-3
y(0)
0
y(10)
20

The above is a nested ternary operator. Ternary does not use elif expressions but rather uses a nested if expression. In the above, the nested if depicts the elif statement shown in the f function. Note that we could also entirely get rid of the if statements by just doing basic computations. This is usually not the case. \((1-a)\mathbb{I}_{(a < -3)} +a\mathbb{I}_{(-3\le a\le 3)} + 2a\mathbb{I}_{(a>3)}\) where \(\mathbb{I}_{(.)} = \begin{cases}1& \text{if condition }(\cdot) \text{ is met}\\ 0 & \text{otherwise}\end{cases}\). In our case, \(\mathbb{I}\) is just the logical value returned by the condition. Refer to question3 exercise 1.

Note that when R parses the statements above, else is taken to be a stand alone and not part of the if

Another example:

x = -3
y = x if x > 0  else 0 # What is the value of y?
y
0

Loops

for - loop

This iterates over items in an iterable container. An iterable is any Python object capable of returning its members one at a time, permitting it to be iterated over. Most common iterables include str list tuple dict items and range

for loop has the following basic form.

for item in iterable:
  perform_action

perform_action is called once for each item in iterable

for i in range(0,3): 
  print(i)
0
1
2

A range is simple a container that holds three basic numbers. The start, stop and step of a sequence. eg range(1,10,2) . You could convert this to a list and therefor enumerate all the values associated to the range:

x = range(1,10,2)
x
range(1, 10, 2)
list(x) # enumerate all values associated with range object x
[1, 3, 5, 7, 9]
list(range(10)) # just input one single number
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
list(range(20, 15, -1)) # negative step
[20, 19, 18, 17, 16]

In all the values of a range, the stop value is excluded from the range. Therefore, range(n) produces the values from 0 to n - 1 . This comes in handy when generating indices to index an iterable. As the indexing in python starts from 0, the maximum index for any iterable, will be the length of the iterable minus 1. Thus we could use range(len(iterable)) and this will provide us will all the indices without getting an error.

Note that we could use membership operator in to determine as to whether an object belongs to a range.

2 in x
False
7 in x
True

2 is not in the range while 7 is in the range. The membership test is done using a formula.

\[ \left(\text{number to test} - \text{start}\right) mod \text{ step} = 0 \]

ie we test for equality to 0:

start = 1
step = 2
(2 - start) % step == 0
False
(7 - start) % step == 0
True

Since the elements of a range are produced on demand, there should be no need of listing all its elements. Assume the range covers tens of billions of elements, your computer does not have memory to list all the elements. eg creating a range object representing a sequence of 1billion numbers takes 48bytes while listing all the objects requires 80GB. Better use the range object.

Think of a range as a formula that generates a sequence while keeping track of the currently generated number. We can thus loop over these numbers and take an action.

Altering values in a list

x = [10, 20, 30, -10]
for index in range(len(x)): # iterating over the indices of x
  x[index] = x[index]**2
x
[100, 400, 900, 100]

creating a new list, while maintaining the old list:

x = [10, 20, 30, -10]
y = []
for element in x: # iterating over the actual elements of x
  y.append(element**2)
x,y
([10, 20, 30, -10], [100, 400, 900, 100])

Apart from range, there is also an enumerated object. This is an object that contains the values of the list together with the associated indices.

x = [10, 20, 30, -10]
list(enumerate(x))
[(0, 10), (1, 20), (2, 30), (3, -10)]

We could thus loop though the enumerated object and carry out a predetermined action.

for idx,val in enumerate(x):
  print(f'Number at position {idx} is {val}. its square is {val**2}')
Number at position 0 is 10. its square is 100
Number at position 1 is 20. its square is 400
Number at position 2 is 30. its square is 900
Number at position 3 is -10. its square is 100

For now we did a simple printing. But sometimes the computation is complex.

The last object to talk about is the zip object. zip is used to bundle two or more elements of iterable objects together as a tuple.

x = [1, 2, 5, 10]
y = [3, 9, 11, 15]
list(zip(x,y))
[(1, 3), (2, 9), (5, 11), (10, 15)]

You could zip as many objects as you want as long as they are all of the same length. If not of the same length, then the zip will ignore the extra elements of the longer object

x = [1, 2, 5, 10]
y = [3, 9, 11]
list(zip(x,y))
[(1, 3), (2, 9), (5, 11)]

In the example above, the element 10 from list x was ignored.

How can we use this in for-loops?

x = [1, 2, 5, 10]
y = [3, 9, 11, 15]
z = []
for elem_x, elem_y in zip(x,y):
  z.append(elem_x + elem_y) 
z
[4, 11, 16, 25]
first_name = ['Joe','Earnst','Thomas','Martin','Charles']
last_name = ['Schmoe','Ehlmann','Fischer','Walter','Rogan','Green']
age = [23, 65, 11, 36, 83]

for first_name, last_name, age in zip(first_name, last_name, age):
    print(f"{first_name} {last_name}\t is {age} years old")
Joe Schmoe   is 23 years old
Earnst Ehlmann   is 65 years old
Thomas Fischer   is 11 years old
Martin Walter    is 36 years old
Charles Rogan    is 83 years old

Example: Computing the factorial of a number. \(n! = 1\times2\times3\cdots\times n\) with the added notion that \(0! = 1\)

def factorial(n):
  result = n
  for i in range(1, n): 
    result *= i
  return result

factorial(6)
720

question: Explain as to why we set the result to n.

Using for loops we could easily write functions to compute sum, max, min, etc. Below are simple examples of sum and max

def my_sum(x):
  results = 0
  for val in x:
    results += val
  return results

my_sum([1,2,3,4,8,9,10])
37
def my_max(x):
  results = x[0]
  for val in x:
    if val > results:
      results = val
  return results

my_max([1,2,3,20,4,8,9,10])
20

Python provides a quirky an exiting pythonic way to manipulate lists.

List comprehensions

These are one liner for loops almost similar in structure to the ternary operator. They are often faster than normal for-loops because they use a more optimized internal mechanism for iterating over the collection. Additionally, they allow you to perform transformations and filtering in a single statement, which can lead to more efficient code.

The syntax looks like shown below:

[perform_action for item in iterable]
x = [1, 2, 5, 10]
y = [3, 9, 11, 15]
z = [elem_x + elem_y for elem_x, elem_y in zip(x,y)]
z
[4, 11, 16, 25]

This resembles the normal for loop introduced earlier. The difference being where the action to be performed is placed.

While all list comprehensions can be written as a for-loop, not all for-loops can be written as a list comprehension. eg Factorial loop above cannot be written as a list comprehension.

Note that list comprehensions can also include branches ie the if and if else conditions. No elif expressions can be used in list comprehension

Suppose we want to filter even values from a list on numbers:

x = [1,6,24,3,7,8,12,13]
[i for i in x if i % 2 == 0]
[6, 24, 8, 12]

Could you explain the code above?

We can also decide to manipulate some numbers in one way and other numbers in another way. For example, if the number is less than 10, square the number otherwise get half the number:

\[ y = \begin{cases} x^2 & x < 10\\ \frac{x}{2}& x\ge10\end{cases} \]

[i**2 if i < 10 else i/2 for i in x]
[1, 36, 12.0, 9, 49, 64, 6.0, 6.5]

Take note that the first part is ternary. We invoke the use of nested ternary expressions. If the code becomes complicated, revert back to using the normal for-loop.

What if we had multiple conditions? Use nested ternary operations.

Dict comprehension

This is similar to the list comprehension, although we create a new dictionary instead of a list:

x = [1,6,24,3,7,8,12,13]
{key:val for key, val in enumerate(x)}
{0: 1, 1: 6, 2: 24, 3: 3, 4: 7, 5: 8, 6: 12, 7: 13}

Note that there must be a mapping between a key and a value prior to the for loop

{i:i**2 for i in x}
{1: 1, 6: 36, 24: 576, 3: 9, 7: 49, 8: 64, 12: 144, 13: 169}

While Loop

Unlike the for-loop, this takes a condition and as long as the condition is satisfied, the iteration is carried out.

while condition:
  perform_action
count  =  0
while count < 5:
  print(count)
  ## Compute other stuff in here
  count += 1 # increase x by 1
0
1
2
3
4

Notice that The increment statement is at the very end. Depending on the nature of the problem, you can change the position of the increment statement and the condition for the loop to terminate.

All for loops can be translated to while loops but not all while loops could be translated to a for loop. The main difference between the for loop and while loop is that the number of iterations to be carried out in a for loop is predetermined where as that is not the case in a while loop.

We could write all the for-loops above as while loops. Lets take a look at the factorial function

def factorial2(n):
  results = 1
  while n > 1:
    results *= n
    n -= 1
  return results

factorial2(6)
720

An example where a for loop would not work:

Consider the following operation on an arbitrary positive integer:

  • If the number is even, divide it by two.

  • If the number is odd, triple it and add one.

In modular arithmetic notation, define the function f as follows:

\[ {\displaystyle f(n)={\begin{cases}n/2&{\text{if }}n\equiv 0{\pmod {2}},\\[4px]3n+1&{\text{if }}n\equiv 1{\pmod {2}}.\end{cases}}} \]

The question then becomes, given \(n\) list all the intermediate numbers until you get to 1.

For instance, starting with \(n = 12\)  and applying the function \(f\) , one gets the sequence \(12, 6, 3, 10, 5, 16, 8, 4, 2, 1\)

Since the number of iterations is not known beforehand, we cannot use a for-loop. We are left to use a while-loop:

def colletz(n):
  results = [n]
  while n > 1:
    n = n//2 if n % 2 == 0 else 3*n + 1
    results.append(n)
  return results
colletz(12)
[12, 6, 3, 10, 5, 16, 8, 4, 2, 1]
colletz(19)
[19, 58, 29, 88, 44, 22, 11, 34, 17, 52, 26, 13, 40, 20, 10, 5, 16, 8, 4, 2, 1]

Note that different n takes different number of iterations. For example \(n = 12\) takes 9 iterations while \(n=27\) takes \(111\) iterations.

Loop Termination

There are two ways to terminate a loop early:

continue

Exits the current iteration and continues to the next iteration:

for i in range(5):
  if i == 3: continue
  print(i)
0
1
2
4

Notice how the number 3 was not printed. That is because at i=3 we skipped the iteration and went to the next iteration. Often this code is not used as it wastes resources. But in case you find yourself not being able to avoid it, then go ahead and use it.

break

Exits the entire for/while loop.

x = 3
while x<10:
  print(x)
  if x == 7: break
  x += 1
3
4
5
6
7

What really is the benefit of break? Note that in the above example, we can just to a for loop of elements from in range(3, 8). Although it depicts the picture of what break does, it don not capture the whole essence.

Suppose we want to compute the value of \(x\) such that \(x^3 = 27\). How will we go about it?

Assume we are given that the update to the problem is \(x_{n+1} = \frac{x_n^3 + 27}{2x_n^2}\)

of course we know that the solution of the equation is 3. ie \(3^3=27\) . But we could use iterative methods with the given updating formula to solve the problem. Below is how the problem will look like

x = 10 #initial guess value. We assume that 10 is the solution. Then solve from there
while True:
  x_new = (x**3 + 27) /(2 * x**2)
  if abs(x_new - x) < 1e-5: break
  x = x_new
  print(x)
5.135
3.0794798545408346
2.963310604450811
3.019028899041687
2.990665080839297
3.0047112114109655
2.9976554688592474
3.001175016850196
2.9994131815468656
3.000293581449432
2.9998532523646957
3.0000733845857885
2.999963310399667
3.000018345473241
2.9999908274316565
3.00000458632624

Note that we are interested in ensuring that the absolute difference between \(x\) and the \(x_{new}\) is less than some very small number \(\epsilon = 10^5\). In most cases this is considered to be the tolerance level of the error. The smaller the number, the more accurate our results will be. In this case, we get some number close to \(3\) but not quite enough. Lets change that:

x = 10
while True:
  x_new = (x**3 + 27) /(2 * x**2)
  if abs(x_new - x) < 1e-16: break
  x = x_new
x
3.0

Here we see that the results is exactly 3. This is due to the small tolerance level used. ie \(10^{-16}\) .

The while loop depicted above, without the break condition, is infinite. That is because its initial condition ie while True will always be TRUE. We thus need a condition to exit the loop. We provide the condition using an if statement and then use the break statement to exit the loop.

The same code above could be written in different ways:

x = 10
err = 1
while err > 1e-16: 
  x_new = (x**3 + 27) /(2 * x**2)
  err = abs(x_new - x)
  x = x_new
x
3.0

A very unconventional way to do the same is by using the assignment expression operator :=

x = 10
err = 1
while err > 1e-16: 
  err = abs(x - (x := (x**3 + 27) /(2 * x**2)))
x
3.0
x = 10
while True: 
  if abs(x - (x := (x**3 + 27) /(2 * x**2))) < 1e-16: break
x
3.0
x = 10
while abs(x - (x := (x**3 + 27) /(2 * x**2))) > 1e-16: pass
x
3.0

NB: The update given above is problem specific. To learn how to get the updates look at root finding methods

Loop Examples

def cumsum(seq): # cummulative sum of a sequence.
  result = [0]
  for i in seq:
    result.append(result[-1] + i)
  return result[1:]
  
cumsum(range(1, 11))
[1, 3, 6, 10, 15, 21, 28, 36, 45, 55]
def cumsum2(seq):
  result = [seq[0]]
  for i in seq[1:]:
    result.append(result[-1] + i)
  return result
  
cumsum2(range(1, 11))
[1, 3, 6, 10, 15, 21, 28, 36, 45, 55]

Quick Lesson on Control Characters [2]for printing on screen.

These characters enable one to neatly print elements on the screen.

  • \b backspace- Deletes one element to the left- Moves cursor one element to the left

    print("This iss\b good") # One s has been deleted
    This iss good
  • \n new line/line feed - prints the next text in a new line. Moves cursor to a new line.

    print("First line\nSecond Line\n\nThiiird line")
    First line
    Second Line
    
    Thiiird line
  • \r carriage return - Moves cursor to the begining of the current line

    print("This replaced by \rthat \n")
    This replaced by 
    that 
  • \t tab - Moves cursor 4-spaces ahead

    print("Hello\tWorld\n")
    Hello   World

Multiplication Table.

print("\t------------------------------------------")
    ------------------------------------------
print("\t1\t 2\t 3\t 4\t 5\t 6")
    1    2   3   4   5   6
print("\t------------------------------------------")
    ------------------------------------------
for i in range(1, 7):
  print("\t", i, end='')
  for j in range(1, 7):
    print("\t", j * i, end='')
  print()
     1   1   2   3   4   5   6
     2   2   4   6   8   10  12
     3   3   6   9   12  15  18
     4   4   8   12  16  20  24
     5   5   10  15  20  25  30
     6   6   12  18  24  30  36

Of course we can control the number of digits/ the width of the text to be printed using sprintf function.

Notice how I used extra curly braces {} to wrap the whole code as one. By removing the outermost braces, the behavior changes. The code needs to be run as a single unit and that’s why we put the extra braces

Exercise 2

Submit work here

  1. Write a function that prints a multiplication table. Use double for-loops:

  2. In exercise 1, we had used various functions to write a my_max function. Having learned about control flows, write a function to compute the maximum of two numbers.

  3. Write a function grade(x) that would take in the marks of a student and output the grade as per the table below eg grade(73) should output C

    A B C D E F
    90-100 80-89 70-79 60-69 50-59 <50
  4. Write a function grades(x) that would take in a list/tuple of scores and output the grades as per the table in question one.

    Input: [73, 92, 80, 49]
    Output: [C, A, B, F]
  5. Write a program parse_number that takes in a string and returns a float number from it. If the string is not a valid number, the function should return nan which is a special placeholder for a float that is not a number. The nan can be obtained by float('nan'). The program should not throw an error. To accomplish the task, you have to ensure that the string contains at most one period and also that apart from the period (if any), everything else is numeric.

    Examples:

    Input: '1234'
    Output: 1234.0
    
    Input: '1234.5'
    Output: 1234.5
    
    Input: '1234.5.6'
    Output: nan
    
    Input: '12a4'
    Output: nan
    
    Input: 'This is not right'
    Output: nan
  6. globals() function returns all the variables currently present in your work space/environment. Using a dict comprehension, filter and output only the variables that do not begin with an underscore.

    Example

    a = 1
    b = 3
    d = range(5)
    Output: {'a':1, 'b':3, 'd':range(5)}
  7. dir(str) outputs all the methods in the str class. List all methods accessible to the user. ie methods that do not start with an underscore. Output should be similar to the list shown in introduction page on str methods

  8. \(\mathbf{\pi}\) Estimation: Using the formula: \(\pi = 4 - \frac{4}{3} + \frac{4}{5} - \frac{4}{7} - \frac{4}{9}+\cdots\) use the first 100,000 terms to estimate \(\pi\). Does it converge?

    Hint: \(\pi = 4\sum_{i=1}^n \frac{(-1)^{i-1}}{2i-1}\)

  1. The sequence above converges slowly. For faster convergence we use Ramanujan’s equation for \(\pi\) ie: \({\frac {1}{\pi }}={\frac {2{\sqrt {2}}}{9801}}\sum _{k=0}^{\infty }{\frac {(4k)!(1103+26390k)}{(k!)^{4}396^{4k}}}\) . Use the first 5 terms to estimate \(\pi\) . You can print more decimals on the screen by using print(f'{pi:.22f}') or using . (You should watch the movie - “The man who knew Infinity”)

  2. Square root -Average Method: To compute the square root of a number, one can often use an iterative approach of line search.

    Assume we want the \(\sqrt{2}\). Let our initial points be 0 and 2, then we could do:

    \[ \begin{aligned} \operatorname{begin} &= 0, \quad\operatorname{end}=2\\ x_1 &= \frac{0 + 2}{2} = 1 ~~ie~~ \text{midpoint between }0 \text{ and }2 \\ \operatorname{error} &= 2-1 = 1\\ \text{Since error}&>0, \operatorname{begin=}1\quad x_2 = \frac{1+2}{2} = 1.5 ~~ie~~ \text{midpoint between }1 \text{ and }2 \\ \operatorname{error} &= 2-1.5^2 = -0.25\\ \text{Since error}&<0, \operatorname{end=}1.5\quad x_3 = \frac{1+1.5}{2} = 1.25\\ \operatorname{error} &= 2-1.25^2 = 0.4375\\ \text{Since error}&>0, \operatorname{begin=}1.25\\ x_4 &= \frac{1.25+1.5}{2} = 1.375\\ \vdots \end{aligned} \]

    This algorithm converges slowly. We continue averaging until the \(|\epsilon|\leq\tau\) where \(\tau\) is taken to be a very small number for example \(\tau =10^{-8}\) =1e-8. Write a function sqrt_avg(x) implementing the algorithm above.

  3. Square root - Heron’s Method-: To calculate \(\sqrt{S}\) we use the equation
    \[ x_{n+1} = \frac{1}{2}\left(x_n + \frac{S}{x_n}\right) \]

    where \(x_0\) is any arbitrary positive integer. The closer it is to \(\sqrt{S}\) the better.

    Given \(S=125348\) ,we can estimate to six significant figures the \(\sqrt{S}\) . using the rough estimation method above to get

    \[ \begin{array}{ll}x_0=6 \cdot 10^2 & =600.000 \\x_1=\frac{1}{2}\left(x_0+\frac{S}{x_0}\right)=\frac{1}{2}\left(600.000+\frac{125348}{600.000}\right)=404.457 \\x_2=\frac{1}{2}\left(x_1+\frac{S}{x_1}\right)=\frac{1}{2}\left(404.457+\frac{125348}{404.457}\right)=357.187 \\x_3=\frac{1}{2}\left(x_2+\frac{S}{x_2}\right)=\frac{1}{2}\left(357.187+\frac{125348}{357.187}\right)=354.059 \\x_4=\frac{1}{2}\left(x_3+\frac{S}{x_3}\right)=\frac{1}{2}\left(354.059+\frac{125348}{354.059}\right)=354.045 \\x_5=\frac{1}{2}\left(x_4+\frac{S}{x_4}\right)=\frac{1}{2}\left(354.045+\frac{125348}{354.045}\right)=354.045\end{array}\\ \therefore \sqrt{125348} \approx 354.045 \]

    Note that \(x_0\) does not necessarily have to be the number given above. It can be any positive number eg 1. Using the algorithm described above, write a function named sqrt_heron(x) that takes in one parameter and computes the square root of any non-negative real number. Use a tolerance level of 1e-8

Task 2 - Leetcode Questions

  1. Reverse Integer: Given a signed 32-bit integer x, return x with its digits reversed. If reversing x causes the value to go outside the signed 32-bit integer range \([-2^{31}, 2^{31} - 1]\) then return 0.

    Input: x = 123
    Output: 321
    
    Input: x = -123
    Output: -321
    
    Input: x = 120
    Output: 21
  2. Longest Sub-string: Given a string s, find the longest sub string without repeating characters. Note that this is abbit different from the original question in that we return the sub string rather than the length of the sub string.

    Input: s = "abcabcbb" 
    Output: "abc"
    
    Input: s = "bbbbb" 
    Output: "b"
    
    Input: s = "pwwkew"
    Output: "wke"
    
    Input: s = "pwwkewesdal"
    Output: "wesdal""
Back to top