Program Flow
Here we want to introduce the basic concepts of program flow that occur in almost every programming language.
To implement a logical program flow, we need conditional statements and loops.
Furthermore, we will learn how to implement our own functions to better structure the program flow and reuse frequently used code.
Logical Operators¶
Logical operators are used in if statements and loops to control the program flow:
| Operator | Meaning | |
|---|---|---|
a==b | a and b are equal | |
a < b | a is less than b | analogous to > |
a <= b | a is less than or equal to b | analogous to >= |
a != b | a is not equal to b |
Comparison operations always return an object of type bool, i.e., either True or False:
b = 1<2 ##en
print("The result of 1<2 is of type", type(b), "and has the value", b)The result of 1<2 is of type <class 'bool'> and has the value True
Comparison operators can only be used if these operations are defined for the data types being compared. The following comparisons work:
# int-int #en
print("1 is less than 3:", 1<3)
# string-string (compare alphabetical order)
print("'abc' is less than 'bcd':", 'abc'<'bcd')
# int-float
print("2 is greater than or equal to 2.0:", 2 >= 2.0)1 is less than 3: True
'abc' is less than 'bcd': True
2 is greater than or equal to 2.0: True
For complex numbers, there is no comparison operation such as < or >.
(1+3j)<(2-4j)---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[3], line 1
----> 1 (1+3j)<(2-4j)
TypeError: '<' not supported between instances of 'complex' and 'complex'Boolean expressions can also be combined. The equivalents of the mathematical expressions , , and are:
| Operator | Meaning |
|---|---|
a and b | returns True if both a and b are True |
a or b | returns True if either a or b is True |
not a | returns True if a is False |
Example: We want to test whether a number lies in the interval :
import random
# Generate a random number between 0 and 1
x = random.random()
# Test whether x lies in the interval [1/4, 3/4):
in_interval = x >= 1/4 and x < 3/4
# Print to console
print("x =", x)
print("x lies in [1/4, 3/4):", in_interval)x = 0.38517796941543214
x lies in [1/4, 3/4): True
Conditional Statements (if-statements)¶
Conditional statements in Python can be implemented as follows:
if <condition_1>:
# If condition_1 is True:
[do something]
...
[do something more]
elif <condition_2>:
# If condition_1 is False and condition_2 is True:
[do something]
...
[do something more]
else:
# If both condition_1 and condition_2 are False:
[do something]
...
[do something more]Of course, the statement can be extended with any number of elif blocks.condition_1 and condition_2 must be Boolean variables, i.e., either True or False. The indentation of the code under the if statement determines what actions are executed when condition_1 is True. The code block that is executed ends with the last indented line. See the output of the following code for illustration:
x = 0.8
if x < 0.5:
print("I belong to the conditional block")
print("Me too")
if x < 0.5:
print("I belong to the conditional block")
print("Not me") # Only this line will be executedNot me
x = 0.2
if x < 0.5:
print("I belong to the conditional block")
print("Me too")
if x < 0.5:
print("I belong to the conditional block")
print("Not me")I belong to the conditional block
Me too
I belong to the conditional block
Not me
In the following example, a random number between 1 and 10 is generated and tested to see whether it is even or odd. We will also learn about the modulo operator %, which returns the remainder of the division of two integers. Example: 8%3 equals 2, since . This process is repeated 10 times. An explanation of for-loops follows in the next section.
for k in range(10):
x = random.randint(1,11)
if x % 2 == 0:
print(x, "is an even number")
else:
print(x, "is an odd number")7 is an odd number
7 is an odd number
8 is an even number
11 is an odd number
10 is an even number
11 is an odd number
8 is an even number
5 is an odd number
7 is an odd number
8 is an even number
Loops¶
for-loops¶
To understand the need for loops, let’s first consider the following code, which calculates the sum of numbers from a list:
a = [1,4,7,4,2]
value = 0
value += a[0]
value += a[1]
value += a[2]
value += a[3]
value += a[4]
print("The sum of the numbers", a, "is", value)The sum of the numbers [1, 4, 7, 4, 2] is 18
Here, we obviously want to iterate over all elements of a list. This becomes problematic when the list contains several thousand elements. Our code would then grow to several thousand lines. Moreover, we have here relied on knowing the number of elements in the list. The lines value += a[i] only differ by the index i, and loops allow us to combine all these lines into a single structure.
The first type of loop we want to discuss here is the for loop. The general syntax is:
for <elem> in <iterable_object>:
[do something]
...
[do something more]<iterable_object>can be any object that provides an iterator (more on this in section Iterators)Examples: list, a tuple, or NumPy array (see section Linear Algebra with NumPy), string
The above example can be simplified using a for loop:
value = 0
for elem in a:
value += elem
print("The sum of the numbers", a, "is", value)The sum of the numbers [1, 4, 7, 4, 2] is 18
Here, the line value += elem is repeated as many times as the list contains elements. The variable elem takes on the values a[0], a[1], ..., a[4] sequentially.
In addition to the iterable data types mentioned above, there is the range class. A range object represents an interval for the iteration index with a start and end index. From the Python documentation (accessible via range?) we learn, for example:
range(stop) -> range object
range(start, stop[, step]) -> range object
Return an object that produces a sequence of integers from start (inclusive)
to stop (exclusive) by step. range(i, j) produces i, i+1, i+2, ..., j-1.
start defaults to 0, and stop is omitted!Here is a small test:
# 5 loop iterations
for i in range(5):
print("Loop iteration", i)Loop iteration 0
Loop iteration 1
Loop iteration 2
Loop iteration 3
Loop iteration 4
# 3 loop iterations, starting with iteration index 2
for i in range(2,5):
print("Loop iteration", i)Loop iteration 2
Loop iteration 3
Loop iteration 4
Note: If the number of loop iterations is known before the first iteration, a for loop should be used. Otherwise, use a while loop, which we will discuss in the following section.
Loops can be further controlled within the loop block using the continue and break statements. With break, an additional exit condition can be implemented. In the example, the number 5 is searched for in a list, and the loop is terminated upon the first occurrence of this number.
L = [1,4,5,7,9]
for x in L:
if x == 5:
print("List contains", 5)
break
else:
print(x, "is not 5")1 is not 5
4 is not 5
List contains 5
In the previous example, the loop is terminated with break after encountering the number 5 — it will not continue after the third iteration.
In contrast, continue only skips the current iteration and immediately moves to the next iteration.
Example: Calculate the sum of all even numbers from 1 to 10:
val = 0
for x in range(1,11):
if x % 2 == 1: # if x is odd
continue
val += x
print("2+4+...+10 =", val)2+4+...+10 = 30
while-loops¶
In for loops, the termination condition was known before the first iteration.
If the termination condition changes during the loop, a while loop is used.
The general syntax is:
while <condition>:
[do something]
...
[do something more]<condition>is a Boolean expression.The indented loop block is executed as long as
<condition>isTrue.To avoid an infinite loop, it must be ensured that
<condition>becomesFalseafter a finite number of iterations.
In the following example, random numbers are generated until a number divisible by 5 is produced:
number = 1 # Initialize with 1 to satisfy the loop condition
while not number % 5 == 0:
number = random.randint(1,100)
print("Random number:", number)
print(number, "is our random number divisible by 5")Random number: 90
90 is our random number divisible by 5
Even in while loops, the statements continue and break apply.
Functions¶
Defining Your Own Functions¶
Frequently reused parts of a program can be encapsulated in functions to improve code readability. A function must be defined before its first call.
The general syntax of a function definition is
def <function_name>(<param1>, <param2>[, ...]):
[do something]
...
[do something more]
return <ret1>, <ret2>[, ...]The parameters <param1>, <param2>, etc., are passed when the function is called.
The return values <ret1>, <ret2>, etc., are returned by the function and can, for example, be stored as a tuple.
If the function does not need to return any values, e.g., a function that only prints to the console, the return line can be omitted.
Note: When return is called, the function exits immediately. Any subsequent code inside the function will not be executed.
The syntax of a function call is
<ret1>, <ret2>[, ...] = <function_name>(<param1>, <param2>[, ...])Let’s start with a simple example. The following function sums all elements of an iterable object L and returns the result:
def sum(L):
val = 0
for elem in L:
val += elem
return valTo execute this function, we write:
#en
L = [1,4,7]
sum_L = sum(L)
print("The sum of the numbers", L, "is", sum_L)
L.pop()
L.append(9)
L.append(13)
sum_L = sum(L)
print("The sum of the numbers", L, "is", sum_L)The sum of the numbers [1, 4, 7] is 12
The sum of the numbers [1, 4, 9, 13] is 27
Note that we can call our sum function with parameters of any type for which all operations used in sum are defined.
Our sum function can therefore also be called with tuples:
sum((1,2,3))6Not with strings, however. Strings are also iterable, which allows their use in a for loop, but the += operation between an integer (note: val is an integer due to the line val = 0) and a string is not defined:
sum("Test")---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[18], line 1
----> 1 sum("Test")
Cell In[15], line 4, in sum(L)
2 val = 0
3 for elem in L:
----> 4 val += elem
5 return val
TypeError: unsupported operand type(s) for +=: 'int' and 'str'Local and Global Variables¶
It should be noted that all variables defined inside a function are only local variables. They are therefore not visible outside the function.
If a global variable with the same name already exists, the function will work only with the local variable:
value = 1 # global variable named 'value'
def get_successor(a):
value = a + 1 # local variable named 'value'
return value
print("Value is", value) # prints the global variable 'value'
print("The successor of 3 is", get_successor(3))
print("Value is", value) # prints the global variable 'value'Value is 1
The successor of 3 is 4
Value is 1
In the previous example, the local variable value was only created by the line value = ....
If this line is missing, for example in a function that only reads but does not modify the value of value, the function will use the global variable with the same name:
value = 1
def print_global_variable():
print("Value is", value) # prints the global variable
print_global_variable()Value is 1
If you want to modify a global variable inside a function, you must clarify at the beginning of the function that no new local variable should be created. This is done using the global keyword:
value = 1
def increment_value():
global value
value = value + 1 # writes to the global variable 'value'
print("Value is", value)
increment_value()
print("Value is", value)Value is 1
Value is 2
This approach should, however, be avoided whenever possible. Other functions might rely on the value of the global variable not changing unexpectedly.
What output does this program produce?
Explain why the value of the global variable
valuedoes not change.Modify the function so that the global variable is actually changed.
Mutable vs. Immutable¶
Another interesting question is: What happens to function parameters if we modify them inside a function?
The answer can be seen with a simple test:
def add_something(a):
a += 5
a = 2
print("a =", a)
add_something(a)
print("a =", a)a = 2
a = 2
Although we modified a inside the function, the value of a outside the function does not change.
This is because when the function add_something is called, a copy of the reference to the value of a is passed. Since a is an immutable data type (integer), the operation a += 5 creates a new object that only the local variable a inside the function refers to.
It gets a bit more confusing, though. In the following example, the function parameter is a list, which is modified inside the function. If the function only worked with a copy, the list L should not have changed after the function call. However, this is clearly not the case:
def append_something(L):
L.append(5)
L = [1,2,3,4]
print("List before function call :", L)
append_something(L)
print("List after function call :", L)List before function call : [1, 2, 3, 4]
List after function call : [1, 2, 3, 4, 5]
Obviously, inside append_something, we did not work with a copy of the list, but directly with the object L.
To understand this behavior, it helps to take a closer look at how variables work in Python. Variables are identifiers for objects in memory.
Using the function id, which returns an identification number of an object, we can see where a variable’s value is stored in memory. In the following example, we create two variables that obviously have the same ID:
a = 5
b = 5
print("a :", id(a))
print("b :", id(b))
print("a and b are the same:", a is b)
b = b + 1
print("a :", id(a))
print("b :", id(b))
print("a and b are the same:", a is b)a : 103280627849712
b : 103280627849712
a and b are the same: True
a : 103280627849712
b : 103280627849744
a and b are the same: False
This can be interpreted as a and b being essentially just names that we bind to an object (here the constant 5, which resides somewhere in memory).
Initially, both a and b point to the same object in memory. Therefore, id(a) and id(b) return the same identification number, and the expression a is b evaluates to True.
If the value of b is then changed (b = b + 1), the following happens:
A new object with the value
6is created.The name
bis bound to this new object.aremains bound to the original object5.
The original object in memory is therefore not modified; instead, a new object is created.
Let’s implement a similar test for list objects:
a = [1,2,3]
b = [1,2,3]
print("a :", id(a))
print("b :", id(b))
print("a and b are the same:", a is b)a : 133909565145216
b : 133909565146688
a and b are the same: False
Here, a and b are lists with exactly the same content, but the names point to different objects in memory. This can be seen from the fact that id(a) and id(b) return different values, and the expression a is b evaluates to False.
Unlike the previously considered integers, a new object is created for each list, even if the content is identical.
Let’s slightly modify the example:
a = [1,2,3]
b = a
print("a :", id(a))
print("b :", id(b))
print("a and b are the same:", a is b)
b.append(4)
print("a :", id(a))
print("b :", id(b))
print("a and b are the same:", a is b)
print("a =", a)
print("b =", b)a : 133909567138176
b : 133909567138176
a and b are the same: True
a : 133909567138176
b : 133909567138176
a and b are the same: True
a = [1, 2, 3, 4]
b = [1, 2, 3, 4]
By assigning b = a, we have bound b to the same object as a. Now we need to be careful. We only modified the object behind b using b.append(4), but since a and b are bound to the same object, this operation also affects a. The position of the object in memory does not change. Thus, the object itself is mutable.
As our examples have shown, an object of type int is immutable, which means it cannot be modified. If the value of an integer is changed, a new object is created, to which the original name is bound:
a = 1
print("a :", id(a))
a+= 1
print("a :", id(a))a : 103280627849584
a : 103280627849616
Lists, on the other hand, are mutable and behave differently:
L = [1,2]
print("L :", id(L))
L.append(3)
print("L :", id(L))L : 133909565149952
L : 133909565149952
This also explains the different behavior when using lists as function parameters. The behavior for immutable data types is easy to understand if we extend our example from above with a few print statements:
def add_something(a_):
print("Function start : a ->", id(a_), " Value =", a_)
a_ += 1
print("Function end : a ->", id(a_), " Value =", a_)
a = 1
print("Before function call : a ->", id(a), " Value =", a)
add_something(a)
print("After function call : a ->", id(a), " Value =", a)Before function call : a -> 103280627849584 Value = 1
Function start : a -> 103280627849584 Value = 1
Function end : a -> 103280627849616 Value = 2
After function call : a -> 103280627849584 Value = 1
The name a is initially bound to the object with the constant 1. The name is passed to the function add_something, now called a_ (for clarity; we could have kept it a), but it is still bound to the same object. Inside the function, an operation is applied to this object, here +=. A copy is created because integers are immutable, and the name a_ now points to this new object containing the constant 2. The name a outside the function add_something remains bound to the original object and is therefore unaffected by what happens inside the function.
If a in the previous example had been a list, i.e., a mutable object, an operation that modifies the list would not change the binding of the name to the object.
Conclusion:
Immutable objects (int, float, string, tuple) → modifications create new objects.
Mutable objects (list, dict, set, …) → modifications directly affect the passed object.
Recursive Functions¶
Recursive functions are functions that call themselves repeatedly, up to a certain termination condition. A typical example would be the following function for calculating the factorial:
def faculty(n):
if n == 1:
# recursion termination condition
return 1
else:
# recursive call
return n * faculty(n-1)
print("5! =", faculty(5))5! = 120
Note that many algorithms can be implemented both recursively and iteratively (using a loop). For efficiency reasons, the iterative variant is usually preferred in this case.