Conditional Execution

City University of Hong Kong

CS1302 Introduction to Computer Programming


import math

from flowcharts import *
from ipywidgets import interact

%reload_ext divewidgets

Motivation

Conditional execution means running different pieces of code based on different conditions. Why do programmers need conditional executation?

For instance, when trying to compute a/b, b may be 0 and division by 0 is invalid.

%%optlite -h 450
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)

Although division by 0 is invalid, multiplication remains valid but it is not printed due to the division error.

Can we skip only the division but not multiplication when b is equal to 0?

def multiply_or_divide(a, b):
    q = a / b if b else "undefined"
    print("a:{}, b:{}, a*b:{}, a/b:{}".format(a, b, a * b, q))


multiply_or_divide(1, 2)
multiply_or_divide(1, 0)  # multiplication is valid but not shown

The above solution involves a conditional expression

... if ... else ...

that specifies which code block should be executed under what condition.

Boolean expressions

A condition in a Python program is a boolean expression which has a boolean value true or false. The boolean values can also be entered literally as:

True, False

Comparison Operators

The comparison operation is a special boolean expression that returns True if the operands are equal, and False otherwise.

How to compare different values?

Like the equality and inequality relationships in mathematics, Python also has binary comparison/relational operators:

ExpressionTrue iff
x == yx=yx=y.
x < yx<yx<y.
x <= yxyx\leq y.
x > yx>yx>y.
x >= yxyx\geq y.
x != yxyx\neq y.

Explore these operators using the widgets below:

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)}"""
    )

What is the precedence of comparison operators?

1 + 2 >= 3  # (1 + 2) >= 3

Similar to the assignment operations, Python allows multiple comparison operations to be chained together:

2.0 == 2 > 1  # equivalent to (2.0 == 2) and (2 > 1)

What is the associativity?

(2.0 == 2) > 1, 2.0 == (2 > 1)  # not the same as 2.0 == 2 > 1
1 <= 2 < 3 != 4, (1 <= 2) < (3 != 4)
Solution to Exercise 1 #

The second expression indeed does not involve chained comparison. The expressions inside the parentheses are first evaluated to boolean values, both of which are True. Therefore, the entire expression True < True evaluates to False. You can also try evaluating the following expressions for further understanding:

  • False < True
  • - True ** 2 + 1
  • 1 / False

To learn more about the relationship between bool and int, check out PEP 285.

The second expression is not a chained comparison:

  • The expressions in the parentheses are evaluated to boolean values first to True, and so
  • the overall expression True < True is evaluated to False. You may also want to try:
    • False < True
    • - True ** 2 + 1
    • 1 / False

To understand how bool is related to int, see the PEP 285.

# 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))
Solution to Exercise 2 #
  1. Checks whether an integer is equal to a floating point number.
  2. Checks whether two characters are the same.
  3. Checks whether two strings are the same. Note the space character.
  4. Checks whether a character is larger than the order character according to their unicodes.
  5. Checks whether a string is lexicographically smaller than the other string.
  6. Checks whether a character is not equal to an integer.
  7. TypeError because there is no implementation that evaluates whether a string is smaller than an integer.

Is ! the same as the not operator?

Different from C or javascript:

  • We can write 1 != 2 as not 1 == 2 but not !(1 == 2) because
  • ! is not a logical operator. It is used to call a system shell command in IPython.
%%javascript
element.append(1 != 2,', ', !(1 == 2))
!(1 == 2)
!ls  # a bash command that lists files in the current directory

How to compare floating point numbers?

x = 10
y = (x ** (1 / 3)) ** 3
x == y

Why False? Shouldn't (x13)3=x(x^{\frac13})^3=x?

One method of comparing floating point numbers is:

abs_tol = 1e-9
y - abs_tol <= x <= y + abs_tol

where abs_tol, often denoted as δabs\delta_{\text{abs}}, is a positive number called the absolute tolerance.

Why call it absolute tolerance?

Note that the test remains unchanged if we swap x and y:

abs_tol = 1e-9
x - abs_tol <= y <= x + abs_tol

Using the absolute function abs, we can also rewrite the comparison as follows:

abs_tol = 1e-9
abs(x - y) <= abs_tol

Is an absolute tolerance of 1e-9 good enough?

What if we set x = 1e10 instead of 10?

x = 1e10
y = (x ** (1 / 3)) ** 3

abs_tol = 1e-9
abs(x - y) <= abs_tol

Why does the same absolute tolerance fails to tolerate the different between x and y?

Floating point numbers "float" at different scales.

A better way is to use the isclose function from math module.

math.isclose(x, y)

How does isclose work?

For example, we can check whether xx is within a certain percentage of yy:

rel_tol = 1e-9
y * (1 - rel_tol) <= x <= y * (1 + rel_tol)

i.e., for some positive number δrel\delta_{\text{rel}} called the relative tolerance,

x=y±δrely,x=y \pm \delta_{\text{rel}} \lvert y\rvert,

or equivalently,

xyyδrel.\frac{\lvert x-y\rvert}{\lvert y\rvert} \leq \delta_{\text{rel}}.

To make the test symmetric between x and y, the relative tolerance is applied as follows

xymax{x,y}δrel.\frac{\lvert x-y\rvert}{\max\Set{\lvert x\rvert,\lvert y\rvert}} \leq \delta_{\text{rel}}.
rel_tol = 1e-9
x = 1e10
y = (x ** (1 / 3)) ** 3
### BEGIN SOLUTION
abs(x - y) / max(abs(x), abs(y)) <= rel_tol
### END SOLUTION

What if xx and yy are both very close to 0?

x = 1e-15
y = 0

rel_tol = 1e-9
y * (1 - rel_tol) <= x <= y * (1 + rel_tol)

If we want to tolerate such difference, we need to resort to the absolute tolerance. Why?

abs_tol = 1e-9
abs(x - y) <= abs_tol
math.isclose(x, y), math.isclose(x, y, abs_tol=1e-9)
rel_tol, abs_tol = 1e-9, 0.0
x = 1e-15
y = 0
### BEGIN SOLUTION
abs(x - y) <= max(rel_tol * max(abs(x), abs(y)), abs_tol)
### END SOLUTION

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

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< 0 or 100\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 use or and not.
xyx and yx or ynot x
TrueTrueTrueTrueFalse
TrueFalseFalseTrueFalse
FalseTrueFalseTrueTrue
FalseFalseFalseFalseTrue

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
Solution to Exercise 5
  • Expression A evaluates to True because and has higher precedence and so the expression has the same value as True or (False and True).
  • Expression B evaluates to False because and is left associative and so the expression has the same value as (True and False) and True.
  • Expression C evaluates to True because and has a higher precedence and so the expression has the same value as True or (True and False). Note that (True or True) and False evaluates to something False 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.
(You may also use Thonny for a step-by-step evaluations of the following examples.)

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)

Why expression 2 and 3 are not evaluated?

Because True or ... must be True (why?) so Python does not look further. From the documentation:

Short-circuit evaluation of or

The expression x or y first evaluates x;
if x is true, its value is returned;
otherwise, y is evaluated and the resulting value is returned.

verbose(
    "B", verbose(4, True) and verbose(5, False) and verbose(6, True)
)  # (True and False) and True

Why expression 6 is not evaluated?

True and False and ... must be False so Python does not look further.

Short-circuit evaluation of and

The expression x and y first evaluates x;
if x is false, its value is returned;
otherwise, y is evaluated and the resulting value is returned.

Interesting, Python allows logical operators to be applied to non-boolean operands. From the documentation:

All expressions can be boolean

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
  • Empty strings and containers (including strings, tuples, lists, dictionaries, sets and frozensets)

All other values are interpreted as true.

print("You have entered", input() or "nothing")
Solution to Exercise 6 #
  • The code replaces an empty user input by the default string nothing because an 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.
input(1) or input(2) and input(3)
Solution to Exercise 7

Enter ' ', '' and then ''.

Conditional Constructs

Consider writing a program that sorts values in ascending order.
A sorting algorithm refers to the procedure of sorting values in order.

If-Then Construct

How to sort two values?

Given two values are stored as x and y, we want to

  • print(x,y) if x <= y, and
  • print(y,x) if y < x.

Such a program flow is often represented by a flowchart like the following:

sort_two_values_fc1

How to read the flowchart?

A flowchart uses arrows to connects a set of annotated blocks. The rules were first specified by ANSI and later adopted in ISO 5807.

Why use a program flowchart?

A program flowchart is a powerful way of describing an algorithm quickly. Unlike a text-based programming language:

  • The rules governing the program flow can be shown explicitly by arrows.
  • The annotated graphical blocks can convey the meaning faster using visual clues.

How to implements the flowchart in python?

Python provides the if statement to implement the 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:

%%optlite -h 450
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)

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)
### BEGIN SOLUTION
_ = input(1)
if not _:
    _ = input(2)
    if _:
        _ = input(3)
_
### END SOLUTION

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.

sort_two_values_fc2

This can be done by the else clause of the if statement.

%%optlite -h 450
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))
Solution to Exercise 9

A conditional expression must be an expression:

  1. It must give a value under all cases. To enforce that, else keyword must be provided.
  2. 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 because x = is not part of the conditional expression.
### BEGIN SOLUTION
_ if (_ := input(1)) else (_ := input(3)) if (_ := input(2)) else _
### END SOLUTION

Nested Conditionals

Consider sorting three values instead of two. A feasible algorithm is as follows:

sort_three_values_fc

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()

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()
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)