Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
explore-for-students
GitHub Repository: explore-for-students/python-tutorials
Path: blob/main/Lesson 2 - Control structures.ipynb
968 views
Kernel: Python 3 (ipykernel)

Lesson 2 - Control structures

Authors:

  • Yilber Fabian Bautista

  • Keiwan Jamaly

  • Sean Tulin

Control structures are programming building blocks that are used to control the flow of the program. By default, code is evaluated sequentially, from one line of code to the next. However, control structures allow you to run different segments of the code under different circumstances, or the same segment of code multiple times.

Here we consider three types of control structures:

  • conditional statements, or if-else statements

  • for loops

  • while loops

Another useful type of control structure are try-except statements, but these are covered in a later lesson.

Objectives:

By the end of this lecture you will be able to:

  • Identify and use control structures in short programs in particular to:

    • Understand and design flow charts

    • Use conditional statements

    • Differentiate between definite and indefinite iterations

    • Execute definite iterations using python for loops

    • Execute indefinite iterations using python while loops

    • Translate for loops into while loops and vice-versa

    • Use print statements to understand and debug your codes

  • Expand your list comprehension skills

  • Use higher-order functions

Conditional Statements

A conditional statement is a piece of code that is executed in a program if a certain condition is met, otherwise that part of the code is skipped over and not executed. This can be illustrated in the following Flow Chart

from IPython.display import Image Image("Figures/scala_decision_making.jpg")
Image in a Jupyter notebook

The simples statement (as seen from the flow chart) is the if statement. It has the following basic structure in python:

if <condition>: <statement>
  • condition is an expression that evaluates to a Boolean value (True or False).

  • statement is a valid Python statement, which must be indented.

If condition is True, then statement is executed. If condition is false, then statement is skipped over and not executed.

Note that the colon : following condition is required.

Let us see an example:

x = 5 # Change this to something else to see what happens if x > 3: print("x is greater than 3")
x is greater than 3

We can chain multiple conditions together as follows.

x = 4 # Change this value and see what happens if x > 3: print("x is greater than 3") if x % 2 == 0: print("x is even") if x % 2 == 1: print("x is odd")
x is greater than 3 x is even

Now that we have learned how to execute a code if a condition is met, we ask what else can be done. Sometimes, we want to evaluate a condition and take one path if it is true but specify an alternative path if it is not.

else is precisely another conditional statement. In a flow chart the path specification it looks as follows

Image("Figures/if_else_statement.jpg")
Image in a Jupyter notebook

In python this is accomplished with an if and an else clause as follows:

if <condition>: <statement(s)> else: <statement(s)>

In this code, if condition is true, the first suite is executed, and the second is skipped. If condition is false, the first suite is skipped and the second is executed. Either way, execution then resumes after the second suite.

Let us see and specific example.

x = 3 # change the this value and see what happens if x % 3 == 0: print("x is divisible by 3") else: print("x is not divisible by 3")
x is divisible by 3

In the case we have several alternatives in the code, we use one or more elif (short for else if) clauses. Python evaluates each condition in turn and executes the suite corresponding to the first that is true. If none of the expressions are true, and an else clause is specified, then its suite is executed. In a flow chart it looks as follows.

Image("Figures/Else-if.png")
Image in a Jupyter notebook

In python this is implemented in the following way:

if <condition>: <statement(s)> elif <condition>: <statement(s)> elif <condition>: <statement(s)> ... else: <statement(s)>

Let us see an example implemented here.

name = 'Sam' #change this imput and see what happens if name == 'Fred': print('Hello Fred') elif name == 'Xander': print('Hello Xander') elif name == 'Joe': print('Hello Joe') elif name == 'Arnold': print('Hello Arnold') else: print("I don't know who you are!")
I don't know who you are!

To end this section let us mention that conditional statements can be implemented in one line. (This is analogous to list comprehensions as we will see below.) Let us see an example of how this works.

import numpy as np x = -5 x = np.sqrt(x) if x >= 0 else np.sqrt(-x) print(x)
2.23606797749979

The one liner if statement can be the easies understood if we read it from left to right and translate it to English.

  1. np.sqrt(x): square root of x

  2. if x >= 0: if the condition is true

  3. else -np.sqrt(-x): else (if the condition is false) minus square root of -x

It is a little tricky to get used to the syntax but if it is used at the right places, program code will be very readable. In most cases, though, it is more readable (and preferred) to write conditional statements in multiple lines.

Exercise 1

Define a piecewise function f(x) that takes as input a number x and returns

  • e-e if x<1 x < 1

  • π\pi if x=1x = 1

  • sin(x)\sin(x) if x>1x > 1

Make a plot of xx on the xx-axis and f(x)f(x) on the yy-axis, taking a logarithmic spacing in xx from 10510^{-5} to 10510^5. Use a log scale for the xx-axis.

#Write your code here

For loops

This part of the tutorial will learn how to perform definite iteration with a Python for loop.

An iteration is a repetitive execution of the same block of code over and over. There are two types of iteration:

  • Definite iteration, in which the number of repetitions is specified explicitly in advance

  • Indefinite iteration, in which the code block executes until some condition is met In Python, definite iterations are achieved using for loops, whereas indefinite iterations are performed using while loops.

A for loop has the following structure:

for <var> in <iterable>: <statement(s)>

The loop variable var is an iterator which takes on the value of the next element in iterable. The latter corresponds to a collection of objects, for example, a list or tuple, an interval, an array, etc. The statement(s) in the loop body are denoted by indentation, and are executed once for each item in iterable.

Let us see a simple example of a for loop in python

for i in range(10): print(i)
0 1 2 3 4 5 6 7 8 9

Let's break this down having in mind the previous general structure

  • The for loop always begins with the statement for

  • Then you name a variable wich will change its value in the for loop. This variable is the iterator.

  • The keyword in

  • An iterable object (range(10) in the upper example, at which we will look closer later)

Let us see another example.

x = [2,67,2,5,7,324,56,34] for i in x: print(i)
2 67 2 5 7 324 56 34

Let us see a more complex example of a for loop, for which the iterator can take more complicated objects in the iterator.

x = [(1, "hello"), (25, "pencil"), (58, "metronome"), (5, "bike")] for (number, word) in x: print("Number is", number, "and word is", word)
Number is 1 and word is hello Number is 25 and word is pencil Number is 58 and word is metronome Number is 5 and word is bike

As you can see, you individually select the elements of the tuple in a list. This is pretty neat if you want to write clean code. If you are interested, you can read something about enumerate and destructuring.

As we learned from Exercise 1, conditional statements can be use anywhere in a code, and in particular, they are useful in loops. Let us illustrate this with the following example:

Example

We are given the array of integer numbers from 0 to 100, and we want to separate the even and the odd numbers into two different lists. We can do that with a for loop as follows.

given_array = np.arange(101) even = [] odd = [] for k in given_array: if k % 2 == 1: odd.append(k) else: even.append(k) print("The list of even numbers is:", even,"\n") print("The list of odd numbers is:", odd)
The list of even numbers is: [0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 52, 54, 56, 58, 60, 62, 64, 66, 68, 70, 72, 74, 76, 78, 80, 82, 84, 86, 88, 90, 92, 94, 96, 98, 100] The list of odd numbers is: [1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29, 31, 33, 35, 37, 39, 41, 43, 45, 47, 49, 51, 53, 55, 57, 59, 61, 63, 65, 67, 69, 71, 73, 75, 77, 79, 81, 83, 85, 87, 89, 91, 93, 95, 97, 99]

This simple example illustrates how we can combine elements learned in the different lessons, and make our codes more dynamic.

List Comprehensions

In the previous example we used a for loop to create the lists of even and odd numbers. Here we will learn a different method to create list. Let us see it in a simple example.

x = [] for i in range(10): x.append(i**2) print(x)
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

Analog to the one-line conditional statements of previous section, this can be implemented in one line as follows.

x = [i**2 for i in range(10)] print(x)
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

List comprehensions can also be chained together. Let's say you want to store all positions of a two dimensional grid [0,5]×[0,5][0, 5] \times [0,5] into one list.

grid_2_d = [(x, y) for x in range(6) for y in range(6)] print(grid_2_d)
[(0, 0), (0, 1), (0, 2), (0, 3), (0, 4), (0, 5), (1, 0), (1, 1), (1, 2), (1, 3), (1, 4), (1, 5), (2, 0), (2, 1), (2, 2), (2, 3), (2, 4), (2, 5), (3, 0), (3, 1), (3, 2), (3, 3), (3, 4), (3, 5), (4, 0), (4, 1), (4, 2), (4, 3), (4, 4), (4, 5), (5, 0), (5, 1), (5, 2), (5, 3), (5, 4), (5, 5)]

Exercise 2

Using for loops and conditional statements, find all prime numbers between 0 and 100, and store them in one list. In a separate list, store the non-prime numbers.

#write your solution in this cell

Exercise 3

Assume s is a string of lower case characters.

Write a program that counts up the number of vowels contained in the string s. Valid vowels are: 'a' , 'e', 'i', 'o', and 'u'.

For example, if s = 'azcbobobegghakl', your program should print:

Number of vowels: 5

# write your solutions here

You can learn more about for loops here.

While loops

So far we have learnt how to perform definite iterations using for loops. In this part of the tutorial we will learn how to do do indefinite iterations using while loops.

A while loop has the following structure:

while <condition>: <statement(s)>

Like for loops, the statement(s) represents the block to be repeatedly executed.

The controlling condition typically involves one or more variables that are initialized prior to starting the loop and then modified somewhere in the loop body.

When a while loop is encountered, condition is first evaluated in Boolean context. If it is true, the loop body is executed. Then condition is checked again, and if still true, the body is executed again. This continues until conditionbecomes false, at which point program execution proceeds to the first statement beyond the loop body.

Let us see an example of a while loop:

x = 10 while x > 3: print(x) x -= 1
10 9 8 7 6 5 4

As you can see, the condition is checked before the loop body is executed. It starts with the keyword while. After the keyword, you write the condition (i.e. x > 3), which can be any expression that evaluates to a boolean value, followed by a colon and the code that is executed if the condition is true (i.e., print("x is greater than 3")).

You may have noticed, that this could easily end up in an infinite loop, if you make mistakes during programming, this is very common when first learning how to code while loops, for instance if you forget to update the condition in the loop body.

For instance, if you wrote the following code

x = 10 bigger_3 = True while bigger_3: print(x) x -= 1

this ends in an infinite loop, so please do not try to run it in a cell. We can however truncate infinite loops, using conditional statements. Here is where they become very useful:

x = 10 bigger_3 = True while bigger_3: print(x) x -= 1 if x < 3: break

Try and run this last piece of code in a cell.

Let us finally mention that every for loop can be written in as a while loop: Let us see this in one of our examples above which separates the even an odd numbers contained in the interval 0-100:

x = 100 even = [] odd = [] while x >= 0: if x % 2 == 1: odd.append(x) else: even.append(x) x -= 1 even.sort() # sort() function just orders the numbers from the smallest to the biggest odd.sort() print("The list of even numbers is:", even) print() print("The list of odd numbers is:", odd)
The list of even numbers is: [0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 52, 54, 56, 58, 60, 62, 64, 66, 68, 70, 72, 74, 76, 78, 80, 82, 84, 86, 88, 90, 92, 94, 96, 98, 100] The list of odd numbers is: [1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29, 31, 33, 35, 37, 39, 41, 43, 45, 47, 49, 51, 53, 55, 57, 59, 61, 63, 65, 67, 69, 71, 73, 75, 77, 79, 81, 83, 85, 87, 89, 91, 93, 95, 97, 99]

Exercise 4

Write your code for finding the prime numbers in the interval 0-100 using a while loop

#Write your solutions here

Exercise 5

Here is some code sample:

iteration = 0 count = 0 while iteration < 5: for letter in "hello, world": count += 1 print("Iteration " + str(iteration) + "; count is: " + str(count)) iteration += 1
Iteration 0; count is: 12 Iteration 1; count is: 24 Iteration 2; count is: 36 Iteration 3; count is: 48 Iteration 4; count is: 60

We wish to re-write the above code, but instead of nesting a for loop inside a while loop, we want to nest a while loop inside a for loop. Write a solution for this here:

# Incert your solution here and test it

Exercise 6 (optional)

Use the Newton-Raphson method and a while loop, to find the root of the function f(x)=x3x51f(x) = x^3 - x^5 - 1.

After an initial guess x0x_0, the root can be approximated by the following formula iteratively:

xn+1=xnf(xn)/f(xn)x_{n+1} = x_n - f(x_n) / f'(x_n)

where f(x)f(x) is the function and f(x)f'(x) is the derivative of the function.

Hint: think of a good condition for the while loop.

# write your solution here

A warning about lists and arrays

If you want to copy a single variable, it is trivial to do so. Note that x and y are independent. Although y was initially set to the same value of x, you are then free to modify y without affecting x.

x = 4 y = x print("x=", x, "y=", y) # and if you change y y = 5 print("x=", x, "y=", y)
x= 4 y= 4 x= 4 y= 5

But this does not work for lists or numpy arrays! The following example illustrates the issue.

x = [1,2,3,4,5] y = x y[3] = 100 print("x =", x) print("y =", y)
x = [1, 2, 3, 100, 5] y = [1, 2, 3, 100, 5]

Note that x and y are not independent objects. Setting y = x does not create y as a new list that is the same as x. It just creates a view of x, which means that y and x are actually the same object. Hence, modifying one modifies the other.

Array slicing

Lists and numpy arrays can be sliced, with the same notation that we learned about for slicing strings in Lesson 1. List and array slicing can also help with a quick fix to the above problem, as we will see.

The general syntax of array slicing is

sliced_array = original_array[start:end:step]

where start and end are the start and end index of the slice, and step is the step size. If you omit start, it defaults to 0. If you omit end, it defaults to the length of the array. If you omit step, it defaults to 1. If you omit both start and end, it defaults to the entire array.

Note that end index is not included in the slice. So if you want to select the first three elements of an array, you have to write

silced_array = original_array[0:3]

Keep in mind, that this works also for lists as well as numpy arrays.

To get back to our previous problem, we can simply copy an array using the following syntax:

x = y[:]

Setting y equal to a slice of x (even if that slice is the entire list or array) creates a separate copy of x that can modified separately.

x = [1,2,3,4,5] y = x[:] y[3] = 100 print("x =", x) print("y =", y)
x = [1, 2, 3, 4, 5] y = [1, 2, 3, 100, 5]

range() - function

remember the range() function we have used before? It is a built-in function that returns a sequence of numbers. The sequence of numbers starts at 0 and ends at the number you specify. But you can also specify the start and step size of the sequence. The syntax is very similar to the one of array slicing, but the start and step size are optional and instead of an double colon, you use a comma. Let's generate a more advance sequence, that starts at 10 and ends at 20 with a step size of 2.

range(10, 20, 2)
range(10, 20, 2)

As you can see, the sequence is not really a list, but a range object. You can use the list methods on this object. Let's convert the sequence to a list, to see our results.

list(range(10, 20, 2))
[10, 12, 14, 16, 18]

You can also generate reversed sequences

list(range(20, 10, -2))
[20, 18, 16, 14, 12]

Higher order functions (optional)

Higher order functions are functions that take other functions as arguments or return functions. We will look at two in particular: map and filter.

map()

It takes a function and a sequence as arguments and applies the function to each element of the sequence. An example of this would be:

def square(x): return x**2 squared_sequence = map(square, range(10)) squared_sequence
<map at 0x1e4fc1b4d30>

map() will not return a list, but a a generator object. This means, that elements of the sequence are not computed immediately, but only when you call them within a loop for example. This reduces the memory usage of your program.

for element in squared_sequence: print(element)
0 1 4 9 16 25 36 49 64 81

You can always convert a generator object to a list using the list() function.

We already know the numpy equivalent of map(), we just insert the array into the function.

numpy_array = np.array(list(range(10))) numpy_array_squared = square(numpy_array) print(numpy_array_squared)
[ 0 1 4 9 16 25 36 49 64 81]

filter()

As the name suggests, filter() takes a function and a sequence as arguments and returns the part of the sequence for which the function returns true. let's find all even values of a sequence/list.

x = range(10) def even_check(x): return x % 2 == 0 filtered_sequence = filter(even_check, x) list(filtered_sequence)
[0, 2, 4, 6, 8]

The numpy equivalent of filter() is more complicated. It's best explained, by making an example.

x = np.arange(10) # define our numpy array filter_array = x % 2 == 0 # define our filter print(filter_array) filtered_array = x[filter_array] # apply the filter print(filtered_array)
[ True False True False True False True False True False] [0 2 4 6 8]

As you can see, from our x array, only the elements are selected, where the filter_array is True. you can compact this code to:

x = np.arange(10) x[x % 2 == 0] # apply the filter
array([0, 2, 4, 6, 8])

If you want to write more complicated logical expressions, you might want to use this reference.