4. Conditional Execution¶
4.1. Motivation¶
Conditional execution means running different pieces of code based on different conditions. Why?
For instance, when trying to compute a/b
, b
may be 0
and division by 0
is invalid.
def multiply_or_divide(a, b):
print('a:{}, b:{}, a*b:{}, a/b:{}'.format(a, b, a * b, a / b))
multiply_or_divide(1, 2)
multiply_or_divide(1, 0) # multiplication is valid but not shown
a:1, b:2, a*b:2, a/b:0.5
---------------------------------------------------------------------------
ZeroDivisionError Traceback (most recent call last)
<ipython-input-2-c6f0ad2b2b09> in <module>
4
5 multiply_or_divide(1, 2)
----> 6 multiply_or_divide(1, 0) # multiplication is valid but not shown
<ipython-input-2-c6f0ad2b2b09> in multiply_or_divide(a, b)
1 def multiply_or_divide(a, b):
----> 2 print('a:{}, b:{}, a*b:{}, a/b:{}'.format(a, b, a * b, a / b))
3
4
5 multiply_or_divide(1, 2)
ZeroDivisionError: division by zero
Can we skip only the division but not multiplication when b
is 0
?
def multiply_or_divide(a, b):
fix = a / b if b else 'undefined'
print('a:{}, b:{}, a*b:{}, a/b:{}'.format(a, b, a * b, fix))
multiply_or_divide(1, 2)
multiply_or_divide(1, 0) # multiplication is valid but not shown
a:1, b:2, a*b:2, a/b:0.5
a:1, b:0, a*b:0, a/b:undefined
The above solution involve:
a boolean expression
fix
that checks whether a condition holds, anda conditional construct
... if ... else ...
that specify which code block should be executed under what condition.
4.2. Boolean expressions¶
4.2.1. Comparison Operators¶
How to compare different values?
Like the equality and inequality relationships in mathematics,
Python also have binary comparison/relational operators:
Expression |
True iff |
---|---|
|
\(x=y\). |
|
\(x<y\). |
|
\(x\leq y\). |
|
\(x>y\). |
|
\(x\geq y\). |
|
\(x\neq y\). |
Explore these operators using the widgets below:
# Comparisons
from ipywidgets import interact
comparison_operators = ['==','<','<=','>','>=','!=']
@interact(operand1='10',
operator=comparison_operators,
operand2='3')
def comparison(operand1,operator,operand2):
expression = f"{operand1} {operator} {operand2}"
value = eval(expression)
print(f"""{'Expression:':>11} {expression}\n{'Value:':>11} {value}\n{'Type:':>11} {type(value)}""")
These operators return either
True
orFalse
, which arekeywords
of type boolean.The expressions are called boolean expressions or predicates, named after George Boole.
N.b., the equality operator
==
consists of two equal signs, different from the assignment operator=
.
What is the precedence of comparison operators?
All the comparison operators have the same precedence lower than that of +
and -
.
1 + 2 >= 3 # (1 + 2) >= 3
True
Python allows multiple comparison operations to be chained together:
2.0 == 2>1 #equivalent to (2.0 ==2) and (2>1)
True
What is the associativity?
Comparison operations are non-associative:
(2.0 == 2) > 1, 2.0 == (2 > 1) # not the same as 2.0 == 2 > 1
(False, False)
Errorata in [Halterman17] due to a misunderstanding of non-associativity vs left-to-right evaluation order:
-
The relational operators are binary operators and are all ~left associative~ non-associative.
-
=
should be non-associative instead of right-associative.The corresponding table in
Lecture2/Expressions and Arithmetic.ipynb
should also be corrected accordingly.
Exercise Explain why the following boolean expressions have different values.
1 <= 2 < 3 != 4, (1 <= 2) < (3 != 4)
(True, False)
The second expression is not a chained comparison:
The expressions in the parentheses are evaluated to boolean values first to
True
, and sothe overall expression
True < True
is evaluated toFalse
.
Exercise The comparison operators can be applied to different data types, as illustrated below.
Explain the meaning of the operators in each of the following expressions.
# Comparisons beyond numbers
@interact(expression=[
'10 == 10.', '"A" == "A"', '"A" == "A "', '"A" != "a"',
'"A" > "a"', '"aBcd" < "abd"', '"A" != 64', '"A" < 64'
])
def relational_expression(expression):
print(eval(expression))
Checks whether an integer is equal to a floating point number.
Checks whether two characters are the same.
Checks whether two strings are the same. Note the space character.
Checks whether a character is larger than the order character according to their unicodes.
Checks whether a string is lexicographically smaller than the other string.
Checks whether a character is not equal to an integer.
TypeError because there is no implementation that evaluates whether a string is smaller than an integer.
Is !
the same as the not
operator?
Errata There is an error in Halterman17, p.69 due to confusion with C language:
…
!(x >= 10)
and!(10 <= x)
are ~equivalent~ invalid.
We can write
1 != 2
asnot 1 == 2
but not!(1 == 2)
because!
is not a logical operator. It is used to call a system shell command in IPython.
!(1 == 2)
/bin/bash: 1: command not found
!ls # a bash command that lists files in the current directory
'Conditional Execution.ipynb' Iteration.ipynb
'Conditional Execution.ipynb:Zone.Identifier' Iteration.ipynb:Zone.Identifier
How to compare floating point numbers?
x = 10
y = (x**(1/3))**3
x == y
False
Why False? Shouldn’t \((x^{\frac13})^3=x\)?
Floating point numbers have finite precisions and so
we should instead check whether the numbers are close enough.
One method of comparing floating point numbers:
abs(x - y) <= 1e-9
True
abs
is a function that returns the absolute value of its argument. Hence, the above translates to
or equivalently
where \(\delta_{\text{abs}}\) is called the absolute tolerance.
Is an absolute tolerance of 1e-9
good enough?
What if we want to compare x = 1e10
instead of 10
?
x = 1e10
y = (x**(1/3))**3
abs(x - y) <= 1e-9
False
Floating point numbers “float” at different scales.
A better way to use the isclose
function from math
module.
import math
math.isclose(x, y)
True
How does it work?
math.isclose(x,y)
implements
with the default
relative tolerance \(\delta_{\text{rel}}\) equal to
1e-9
, andabsolute tolerance \(\delta_{\text{abs}}\) equal to
0.0
.
Exercise Write the boolean expression implemented by isclose
. You can use the function max(a,b)
to find the maximum of a
and b
.
rel_tol, abs_tol = 1e-9, 0.0
x, y = 1e-100, 2e-100
### BEGIN SOLUTION
abs(x-y) <= max(rel_tol * max(abs(x), abs(y)), abs_tol)
### END SOLUTION
False
4.2.2. Boolean Operations¶
Since chained comparisons are non-associative. It follows a different evaluation rule than arithmetical operators.
E.g., 1 <= 2 < 3 != 4
is evaluated as follows:
1 <= 2 and 2 < 3 and 3 != 4
True
The above is called a compound boolean expression, which is formed using the boolean/logical operator and
.
Why use boolean operators?
What if we want to check whether a number is either \(< 0\) or \(\geq 100\)?
Can we achieve this only by chaining the comparison operators or applying the logical and
?
# Check if a number is outside a range.
@interact(x='15')
def check_out_of_range(x):
x_ = float(x)
is_out_of_range = x_<0 or x_>=100
print('Out of range [0,100):', is_out_of_range)
and
alone is not functionally complete, i.e., not enough to give all possible boolean functions.In addition to
and
, we can also useor
andnot
.
|
|
|
|
|
---|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
The above table is called a truth table. It enumerates all possible input and output combinations for each boolean operator.
How are chained logical operators evaluated?
What are the precedence and associativity for the logical operators?
All binary boolean operators are left associative.
Precedence:
comparison operators
>not
>and
>or
Exercise Explain what the values of the following two compound boolean expressions are:
Expression A:
True or False and True
Expression B:
True and False and True
Expression C:
True or True and False
Expression A evaluates to
True
becauseand
has higher precedence and so the expression has the same value asTrue or (False and True)
.Expression B evaluates to
False
becauseand
is left associative and so the expression has the same value as(True and False) and True
.Expression C evaluates to
True
becauseand
has a higher precedence and so the expression has the same value asTrue or (True and False)
. Note that(True or True) and False
evaluates to somethingFalse
instead, so precedence matters.
Instead of following the precedence and associativity, however, a compound boolean expression uses a short-circuit evaluation.
To understand this, we will use the following function to evaluate a boolean expression verbosely.
def verbose(id,boolean):
'''Identify evaluated boolean expressions.'''
print(id,'evaluated:',boolean)
return boolean
verbose('A',verbose(1,True) or verbose(2,False) and verbose(3,True)) # True or (False and True)
1 evaluated: True
A evaluated: True
True
Why expression 2 and 3 are not evaluated?
Because True or … must be True (Why?) so Python does not look further. From the documentation:
The expression
x or y
first evaluatesx
; ifx
is true, its value is returned; otherwise,y
is evaluated and the resulting value is returned.
Note that:
Even though
or
has lower precedence thanand
, it is still evaluated first.The evaluation order for logical operators is left-to-right.
verbose('B',verbose(4,True) and verbose(5,False) and verbose(6,True)) # (True and False) and True
4 evaluated: True
5 evaluated: False
B evaluated: False
False
Why expression 6 is not evaluated?
True and False and ...
must be False
so Python does not look further.
The expression
x and y
first evaluatesx
; ifx
is false, its value is returned; otherwise,y
is evaluated and the resulting value is returned.
Indeed, logical operators can even be applied to non-boolean operands. From the documentation:
In the context of Boolean operations, and also when expressions are used by control flow statements, the following values are interpreted as false:
False
, None, numeric zero of all types, and empty strings and containers (including strings, tuples, lists, dictionaries, sets and frozensets). All other values are interpreted as true.
Exercise How does the following code work?
print('You have entered', input() or 'nothing')
The code replaces empty user input by the default string
nothing
because empty string is regarded as False in a boolean operation.If user input is non-empty, it is regarded as True in the boolean expression and returned immediately as the value of the boolean operation.
Is empty string equal to False?
print('Is empty string equal False?',''==False)
Is empty string equal False? False
An empty string is regarded as False in a boolean operation but
a comparison operation is not a boolean operation, even though it forms a boolean expression.
4.3. Conditional Constructs¶
Consider writing a program that sorts values in ascending order.
A sorting algorithm refers to the procedure of sorting values in order.
4.3.1. If-Then Construct¶
How to sort two values?
Given two values are stored as x
and y
, we want to
print(x,y)
ifx <= y
, andprint(y,x)
ify < x
.
Such a program flow is often represented by a flowchart like the following:
Python provides the if
statement to implement the above control flow specified by the diamonds.
# Sort two values using if statement
def sort_two_values(x, y):
if x <= y:
print(x, y)
if y < x: print(y, x)
@interact(x='1', y='0')
def sort_two_values_app(x, y):
sort_two_values(eval(x), eval(y))
We can visualize the execution as follows:
%%mytutor -h 350
def sort_two_values(x, y):
if x <= y:
print(x, y)
if y < x: print(y, x)
sort_two_values(1,0)
sort_two_values(1,2)
Python use indentation to indicate code blocks or suite:
print(x, y)
(Line 5) is indented to the right ofif x <= y:
(Line 4) to indicate it is the body of the if statement.For convenience,
if y < x: print(y, x)
(Line 6) is a one-liner for anif
statement that only has one line in its block.Both
if
statements (Line 4-6) are indented to the right ofdef sort_two_values(x,y):
(Line 3) to indicate that they are part of the body of the functionsort_two_values
.
How to indent?
The style guide recommends using 4 spaces for each indentation.
In IPython, you can simply type the
tab
key and IPython will likely enter the correct number of spaces for you.
What if you want to leave a block empty?
In programming, it is often useful to delay detailed implementations until we have written an overall skeleton.
To leave a block empty, Python uses the keyword pass
.
# write a code skeleton
def sort_two_values(x, y):
pass
# print the smaller value first followed by the larger one
sort_two_values(1,0)
sort_two_values(1,2)
Without pass
, the code will fail to run, preventing you from checking other parts of the code.
# You can add more details to the skeleton step-by-step
def sort_two_values(x, y):
if x <= y:
pass
# print x before y
if y < x: pass # print y before x
sort_two_values(1,0)
sort_two_values(1,2)
4.3.2. If-Then-Else Construct¶
The sorting algorithm is not efficient enough. Why not?
Hint: (x <= y) and not (y < x)
is a tautology, i.e., always true.
To improve the efficient, we should implement the following program flow.
This can be down by the else
clause of the if
statement.
%%mytutor -h 350
def sort_two_values(x, y):
if x <= y:
print(x, y)
else:
print(y,x)
sort_two_values(1,0)
sort_two_values(1,2)
We can also use a conditional expression to shorten the code.
def sort_two_values(x, y):
print(('{0} {1}' if x <= y else '{1} {0}').format(x, y))
@interact(x='1', y='0')
def sort_two_values_app(x, y):
sort_two_values(eval(x), eval(y))
Exercise Explain why the followings have syntax errors.
1 if True
File "<ipython-input-30-1cbab0db8442>", line 1
1 if True
^
SyntaxError: invalid syntax
x = 1 if True else x = 0
File "<ipython-input-31-f68814adf81f>", line 1
x = 1 if True else x = 0
^
SyntaxError: can't assign to conditional expression
A conditional expression must be an expression:
It must give a value under all cases. To enforce that,
else
keyword must be provided.An assignment statement does not return any value and therefore cannot be used for the conditional expression.
x = 1 if True else 0
is valid becausex =
is not part of the conditional expression.
4.3.3. Nested Conditionals¶
Consider sorting three values instead of two. A feasible algorithm is as follows:
We can implement the flow using nested conditional constructs:
def sort_three_values(x, y, z):
if x <= y <= z:
print(x, y, z)
else:
if x <= z <= y:
print(x, z, y)
else:
if y <= x <= z:
print(y, x, z)
else:
if y <= z <= x:
print(y, z, x)
else:
if z <= x <= y:
print(z, x, y)
else:
print(z, y, x)
def test_sort_three_values():
sort_three_values(0,1,2)
sort_three_values(0,2,1)
sort_three_values(1,0,2)
sort_three_values(1,2,0)
sort_three_values(2,0,1)
sort_three_values(2,1,0)
test_sort_three_values()
0 1 2
0 1 2
0 1 2
0 1 2
0 1 2
0 1 2
Imagine what would happen if we have to sort many values.
To avoid an excessively long line due to the indentation, Python provides the elif
keyword that combines else
and if
.
def sort_three_values(x, y, z):
if x <= y <= z:
print(x, y, z)
elif x <= z <= y:
print(x, z, y)
elif y <= x <= z:
print(y, x, z)
elif y <= z <= x:
print(y, z, x)
elif z <= x <= y:
print(z, x, y)
else:
print(z, y, x)
test_sort_three_values()
0 1 2
0 1 2
0 1 2
0 1 2
0 1 2
0 1 2
Exercise The above sorting algorithm is inefficient because some conditions may be checked more than once.
Improve the program to eliminate duplicate checks.
Hint: Do not use chained comparison operators or compound boolean expressions.
def sort_three_values(x, y, z):
if x <= y:
if y <= z:
print(x, y, z)
elif x <= z:
print(x, z, y)
else:
print(z, x, y)
### BEGIN SOLUTION
elif z <= y:
print(z, y, x)
elif z <= x:
print(y, z, x)
else:
print(y, x, z)
### END SOLUTION
sort_three_values(10,17,14)
10 14 17