Programming and Data Analysis¶

Control Flow in Python.

Kuo, Yao-Jen yaojenkuo@ntu.edu.tw from DATAINPOINT

Control Flow¶

What is control flow?¶

In computer science, control flow (or flow of control) is the order in which individual statements, instructions or function calls of an imperative program are executed or evaluated. The emphasis on explicit control flow distinguishes an imperative programming language from a declarative programming language.

Source: https://en.wikipedia.org/wiki/Control_flow

Control flow in short¶

  • Conditional executions
  • Iterations
    • while loop
    • for loop
  • Handling exceptions

Conditional Executions¶

What is a conditional execution?¶

In computer science, conditional statements are features of a programming language, which perform different computations or actions depending on whether a programmer-specified boolean condition evaluates to true or false.

Source: https://en.wikipedia.org/wiki/Conditional_(computer_programming)

Use condition and indentation to create a conditional execution¶

  • A condition is an expression that can be evaluated as bool.
  • Indentation is necessary since Python does not use curly braces for code blocks.
if CONDITION:
    # statements to be executed if CONDITION is evaluated as True.

Most programming languages use braces { } to define a code block¶

  • Python, however, uses indentation.
  • A code block starts with indentation and ends with the first unindented line.
  • The amount of indentation is flexible, but it must be consistent throughout that block.

Use relational or logical operators to produce a bool for condition¶

  • ==, !=, >, <, >=, <=, in, not in
  • and, or, not

Use if for conditional executions¶

if CONDITION:
    # statements to be executed if CONDITION is evaluated as True.

Use a code-visualization tool to help you understand the behavior of conditional executions¶

We can use pythontutor.com to explore the execution of our code.

In [1]:
def return_msg_if_positive(x: int) -> str:
    if x > 0:
        return "{} is positive.".format(x)

print(return_msg_if_positive(5566))
print(return_msg_if_positive(-5566))
5566 is positive.
None

Use if and else to perform alternative executions¶

Since the condition must be true or false, exactly one of the alternatives will run.

if CONDITION:
    # statements to be executed if CONDITION is evaluated as True.
else:
    # statements to be executed if CONDITION is evaluated as False.
In [2]:
def return_msg_whether_positive_or_not(x: int) -> str:
    if x > 0:
        msg = "{} is positive.".format(x)
    else:
        msg = "{} is not positive.".format(x)
    return msg

print(return_msg_whether_positive_or_not(5566))
print(return_msg_whether_positive_or_not(-5566))
5566 is positive.
-5566 is not positive.

Use if, elif, and else to perform chained conditionals¶

The else clause makes our conditionals collectively exhaustive.

if CONDITION_A:
    # statements to be executed if CONDITION_A is evaluated as True.
elif CONDITION_B:
    # statements to be executed if CONDITION_A is evaluated as False and CONDITION_B is evaluated as True.
elif CONDITION_C:
    # statements to be executed if CONDITION_A and CONDITION_B are both evaluated as False and CONDITION_C is evaluated as True.
else:
    # statements to be executed if CONDITION_A, CONDITION_B, and CONDITION_C are all evaluated as False.
In [3]:
def return_msg_whether_positive_negative_or_neutral(x: int) -> str:
    if x > 0:
        msg = "{} is positive.".format(x)
    elif x < 0:
        msg = "{} is negative.".format(x)
    else:
        msg = "{} is neutral.".format(x)
    return msg

print(return_msg_whether_positive_negative_or_neutral(5566))
print(return_msg_whether_positive_negative_or_neutral(-5566))
print(return_msg_whether_positive_negative_or_neutral(0))
5566 is positive.
-5566 is negative.
0 is neutral.

We can break down alternative/chained conditionals once our conditions are mutually exclusive¶

In [4]:
def return_msg_whether_positive_negative_or_neutral(x: int) -> str:
    if x > 0:
        msg = "{} is positive.".format(x)
    if x < 0:
        msg = "{} is negative.".format(x)
    if x == 0:
        msg = "{} is neutral.".format(x)
    return msg

print(return_msg_whether_positive_negative_or_neutral(5566))
print(return_msg_whether_positive_negative_or_neutral(-5566))
print(return_msg_whether_positive_negative_or_neutral(0))
5566 is positive.
-5566 is negative.
0 is neutral.

We can also nest conditionals within other conditionals¶

In [5]:
def return_msg_whether_positive_negative_or_neutral(x: int) -> str:
    if x > 0:
        msg = "{} is positive.".format(x)
    else:
        if x < 0:
            msg = "{} is negative.".format(x)
        else:
            msg = "{} is neutral.".format(x)
    return msg

print(return_msg_whether_positive_negative_or_neutral(5566))
print(return_msg_whether_positive_negative_or_neutral(-5566))
print(return_msg_whether_positive_negative_or_neutral(0))
5566 is positive.
-5566 is negative.
0 is neutral.

If conditions are NOT mutually exclusive in a chained condition¶

  • Still, exactly one of the alternatives will run.
  • But order matters.

Take the famous FizzBuzz for example¶

Fizz buzz (often spelled FizzBuzz in this context) has been used as an interview screening device for computer programmers. Writing a program to output the first 100 FizzBuzz numbers is a trivial problem for any would-be computer programmer, so interviewers can easily filter out those with insufficient programming ability.

Source: https://en.wikipedia.org/wiki/Fizz_buzz

In [6]:
# chained conditionals
def fizz_buzz(x: int) -> str:
    if x % 15 == 0:
        ans = "Fizz Buzz"
    elif x % 3 == 0:
        ans = "Fizz"
    elif x % 5 == 0:
        ans = "Buzz"
    else:
        ans = x
    return ans

fizz_buzz(15)
Out[6]:
'Fizz Buzz'
In [7]:
# non-chained conditionals
def fizz_buzz(x: int) -> str:
    if x % 3 == 0:
        ans = "Fizz"
    if x % 5 == 0:
        ans = "Buzz"
    if x % 15 == 0:
        ans = "Fizz Buzz"
    if (x % 15 != 0) and (x % 3 != 0) and (x % 5 != 0):
        ans = x
    return ans

fizz_buzz(15)
Out[7]:
'Fizz Buzz'

Iterations¶

The essense of iterations¶

Like the slicing syntax of lists:

  • start: when does the iteration start?
  • stop: when does the iteration stop?
  • step: how does the iteration go from start to stop?

We can utilize two kinds of iteration¶

  • while loop
  • for loop

The while loop is used to repeat one or more code statements as long as the condition is evaluated as True¶

i = 0 # start
while CONDITION: # stop
    # repeated statements
    i += 1 # step

Assignment operators are commonly used in writing loops¶

  • i += 1 as in i = i + 1
  • i -= 1 as in i = i - 1
  • i *= 1 as in i = i * 1
  • i /= 1 as in i = i / 1
  • ...etc.

Imgur

Source: A Beginners Guide to Python 3 Programming

The for loop is used to step an element through an iterable until the end is reached¶

for i in ITERABLE: # start/stop/step
    # repeated statements

Imgur

Source: A Beginners Guide to Python 3 Programming

Use a code-visualization tool to help you understand the behavior of loops¶

We can use pythontutor.com to explore the execution of our code.

Retrieving the first 5 odds¶

In [8]:
i = 1 # start
while i < 11: # stop
    print(i)
    i += 2 # step
1
3
5
7
9
In [9]:
for i in range(1, 10, 2): # start/stop/step => help(range)
    print(i)
1
3
5
7
9

Use range() function to create a sequence¶

In [10]:
help(range)
Help on class range in module builtins:

class range(object)
 |  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!  range(4) produces 0, 1, 2, 3.
 |  These are exactly the valid indices for a list of 4 elements.
 |  When step is given, it specifies the increment (or decrement).
 |  
 |  Methods defined here:
 |  
 |  __bool__(self, /)
 |      self != 0
 |  
 |  __contains__(self, key, /)
 |      Return key in self.
 |  
 |  __eq__(self, value, /)
 |      Return self==value.
 |  
 |  __ge__(self, value, /)
 |      Return self>=value.
 |  
 |  __getattribute__(self, name, /)
 |      Return getattr(self, name).
 |  
 |  __getitem__(self, key, /)
 |      Return self[key].
 |  
 |  __gt__(self, value, /)
 |      Return self>value.
 |  
 |  __hash__(self, /)
 |      Return hash(self).
 |  
 |  __iter__(self, /)
 |      Implement iter(self).
 |  
 |  __le__(self, value, /)
 |      Return self<=value.
 |  
 |  __len__(self, /)
 |      Return len(self).
 |  
 |  __lt__(self, value, /)
 |      Return self<value.
 |  
 |  __ne__(self, value, /)
 |      Return self!=value.
 |  
 |  __reduce__(...)
 |      Helper for pickle.
 |  
 |  __repr__(self, /)
 |      Return repr(self).
 |  
 |  __reversed__(...)
 |      Return a reverse iterator.
 |  
 |  count(...)
 |      rangeobject.count(value) -> integer -- return number of occurrences of value
 |  
 |  index(...)
 |      rangeobject.index(value) -> integer -- return index of value.
 |      Raise ValueError if the value is not present.
 |  
 |  ----------------------------------------------------------------------
 |  Static methods defined here:
 |  
 |  __new__(*args, **kwargs) from builtins.type
 |      Create and return a new object.  See help(type) for accurate signature.
 |  
 |  ----------------------------------------------------------------------
 |  Data descriptors defined here:
 |  
 |  start
 |  
 |  step
 |  
 |  stop

Retrieving the first 5 primes¶

In [11]:
primes = [2, 3, 5, 7, 11]
i = 0 # start
while i < len(primes): # stop
    print(primes[i])
    i += 1 # step
2
3
5
7
11
In [12]:
for i in primes: # start/stop/step
    print(i)
2
3
5
7
11

while versus for when dealing with iterations?¶

  • Use for to iterate over lists, dictionaries, and other iterables
  • Use while if our operations involve randomness or uncertainty
In [13]:
from random import randint

def binary_search(low: int=1, high: int=100) -> list:
    guess = (low + high) // 2
    rand_int = randint(low, high)
    guess_history = [guess]
    while guess != rand_int:
        if guess > rand_int:
            high = guess
            guess = (low + high) // 2
        elif guess < rand_int:
            low = guess
            guess = (low + high) // 2 + 1
        guess_history.append(guess)
    return rand_int, guess_history

print(binary_search())
print(binary_search())
print(binary_search())
(85, [50, 76, 89, 82, 86, 84, 86, 85])
(14, [50, 25, 13, 20, 16, 14])
(92, [50, 76, 89, 95, 92])
In [14]:
def int_to_bin_str(x: int) -> str:
    if x < 2:
        return str(x)
    binary_str = ""
    while x > 0:
        modulo = x % 2
        binary_str = str(modulo) + binary_str
        x //= 2
    return binary_str

print(int_to_bin_str(2)) # bin(2)
print(int_to_bin_str(3)) # bin(3)
print(int_to_bin_str(4)) # bin(4)
10
11
100

We've been talking about ITERABLE for quite a few times, so what is a iterable?¶

An iterable is any Python object capable of returning its elements one at a time, permitting it to be iterated over in a for loop. Familiar examples of iterables include lists, tuples, and strings.

Iterate over a str¶

Using built-in functions to explore iterations.

  • iter()
  • next()
In [15]:
help(iter)
Help on built-in function iter in module builtins:

iter(...)
    iter(iterable) -> iterator
    iter(callable, sentinel) -> iterator
    
    Get an iterator from an object.  In the first form, the argument must
    supply its own iterator, or be a sequence.
    In the second form, the callable is called until it returns the sentinel.

In [16]:
help(next)
Help on built-in function next in module builtins:

next(...)
    next(iterator[, default])
    
    Return the next item from the iterator. If default is given and the iterator
    is exhausted, it is returned instead of raising StopIteration.

In [17]:
may4th = "Luke, use the Force!"
I = iter(may4th)
print(next(I))
print(next(I))
print(next(I))
print(next(I))
L
u
k
e

Iterate over a int, float, or bool?¶

In [18]:
def is_x_iterable(x) -> str:
    try:
        I = iter(x)
        return True
    except TypeError as e:
        return e

print(is_x_iterable(5566))
print(is_x_iterable(5566.0))
print(is_x_iterable(False))
print(is_x_iterable(True))
'int' object is not iterable
'float' object is not iterable
'bool' object is not iterable
'bool' object is not iterable

Iterate over a list/tuple is quite straight-forward¶

In [19]:
primes = [2, 3, 5, 7, 11]
for i in primes:
    print(i)
2
3
5
7
11

How about iterating over a dictionary?¶

Use .keys(), .values(), and .items() to help us iterate over a dictionary.

In [20]:
the_celtics = {
    'isNBAFranchise': True,
    'city': "Boston",
    'fullName': "Boston Celtics",
    'tricode': "BOS",
    'teamId': 1610612738,
    'nickname': "Celtics",
    'confName': "East",
    'divName': "Atlantic"
}
print(the_celtics.keys())
print(the_celtics.values())
print(the_celtics.items())
dict_keys(['isNBAFranchise', 'city', 'fullName', 'tricode', 'teamId', 'nickname', 'confName', 'divName'])
dict_values([True, 'Boston', 'Boston Celtics', 'BOS', 1610612738, 'Celtics', 'East', 'Atlantic'])
dict_items([('isNBAFranchise', True), ('city', 'Boston'), ('fullName', 'Boston Celtics'), ('tricode', 'BOS'), ('teamId', 1610612738), ('nickname', 'Celtics'), ('confName', 'East'), ('divName', 'Atlantic')])
In [21]:
for k in the_celtics.keys():
    print(k)
isNBAFranchise
city
fullName
tricode
teamId
nickname
confName
divName
In [22]:
for v in the_celtics.values():
    print(v)
True
Boston
Boston Celtics
BOS
1610612738
Celtics
East
Atlantic
In [23]:
for k, v in the_celtics.items():
    print("{}: {}".format(k, v))
isNBAFranchise: True
city: Boston
fullName: Boston Celtics
tricode: BOS
teamId: 1610612738
nickname: Celtics
confName: East
divName: Atlantic

Common tasks using iteration¶

  • Simply print()
  • Combinations
  • Summations/Counts
  • Iterating over a nested data structure

Common tasks: combinations¶

  • Combining elements as a list.
  • Combining elements as a str.

Combining elements as a list¶

In [24]:
def collecting_two_digit_integers(x: list) -> list:
    out = []
    for elem in x:
        if 10 <= elem <= 99:
            out.append(elem)
    return out

primes = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 101, 103]
collecting_two_digit_integers(primes)
Out[24]:
[11, 13, 17, 19, 23, 29]

Combining elements as a str¶

In [25]:
def collecting_non_vowels(x: str) -> str:
    out = ""
    for char in x:
        char_lower = char.lower()
        if char_lower not in {'a', 'e', 'i', 'o', 'u'}:
            out += char # out = out + char
    return out

print(collecting_non_vowels("Python"))
print(collecting_non_vowels("Anaconda"))
Pythn
ncnd

Common tasks: summations/counts¶

In [26]:
primes = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 101, 103]
summation = 0
counts = 0
for i in primes:
    summation += i  # summation = summation + 1
    counts += 1     # counts = counts + 1

print(summation)
print(counts)
333
12

Common tasks: summations/counts¶

Built-in functions sum() and len() work like a charm.

In [27]:
print(sum(primes))
print(len(primes))
333
12

Common tasks: Iterating over a nested data structure¶

In [28]:
batman_trilogy = {
    "titles": ["Batman Begins", "The Dark Knight", "The Dark Knight Rises"],
    "release_dates": ["June 15, 2005", "July 18, 2008", "July 20, 2012"],
    "director": ["Christopher Nolan", "Christopher Nolan", "Christopher Nolan"]
}
for v in batman_trilogy.values():
    for i in v:
        print(i)
Batman Begins
The Dark Knight
The Dark Knight Rises
June 15, 2005
July 18, 2008
July 20, 2012
Christopher Nolan
Christopher Nolan
Christopher Nolan

Early stop or skipping certain steps in an iteration task¶

  • Use keyword break to early break an iteration
  • Use keyword continue to skip certain steps
In [29]:
def early_break_if_summation_exceeds_fifty(x: range) -> None:
    summation = 0
    for i in x:
        summation += i
        print(i)
        if summation >= 50:
            break

early_break_if_summation_exceeds_fifty(range(100))
0
1
2
3
4
5
6
7
8
9
10
In [30]:
def print_only_evens(x: range) -> None:
    summation = 0
    for i in x:
        if i % 2 == 1:
            continue
        print(i)

print_only_evens(range(10))
0
2
4
6
8

Handling Exceptions¶

Coding mistakes are common, they happen all the time¶

Imgur

Source: Google Search

How does a function designer handle errors?¶

Python mistakes come in three basic flavors:

  • Syntax errors.
  • Runtime errors.
  • Semantic errors.

Syntax errors¶

Errors where the code is not valid Python (generally easy to fix).

# Python does not need braces to create a code block
for (i in range(10)) {
    print(i)
}

Runtime errors¶

Errors where syntactically valid code fails to execute, perhaps due to invalid user input (sometimes easy to fix)

  • NameError
  • TypeError
  • ZeroDivisionError
  • IndexError
  • ...etc.

Source: https://docs.python.org/3/library/exceptions.html

# IndexError
my_favorite_boy_group = "5566"
print(my_favorite_boy_group[4])

Semantic errors¶

Errors in logic: code executes without a problem, but the result is not what you expect (often very difficult to identify and fix)

In [31]:
def product(x: list) -> int:
    """
    x: an iterable.
    Return the product values of x.
    """
    prod = 0 # set 
    for i in x:
        prod *= i
    return prod

print(product([5, 5, 6, 6])) # expecting 900
0

Using try and except to catch exceptions¶

try:
    # sequence of statements if everything is fine
except TYPE_OF_ERROR:
    # sequence of statements if something goes wrong
In [32]:
try:
    my_favorite_boy_group = "5566"
    print(my_favorite_boy_group[4])
except IndexError:
    print("Encountering a IndexError.")
Encountering a IndexError.
In [33]:
# Print out error message directly
try:
    my_favorite_boy_group = "5566"
    print(my_favorite_boy_group[4])
except IndexError as e:
    print(e)
string index out of range
In [34]:
try:
    print(5566 / 0)
except ZeroDivisionError as e:
    print(e)
division by zero
In [35]:
# It is optional to specify the type of error
try:
    print(5566 / 0)
except:
    print("Encountering a whatever error.")
Encountering a whatever error.