# ICP22 lecture notes

## Thursday, September 22 (happy first day of Fall!)

topics for the day:

• review: what we know about `range`(…)
• nested ifs
• multiway ifs
• connecting Booleans with and/or/not

### quick quiz

Consider the following function:

``````def mystery(m):
n = 0
d = 1
for _ in range(m):
if m % d == 0:
n = n + 1
d = d + 1
return n``````
1. What would be the result of evaluating these expressions:

``>>> mystery(4)``

``>>> mystery(5)``

2. Briefly(!), what does the function compute?

answer: Returns the number of divisors of `m`. (Equivalent: Returns the number of factors of `m`.)

#### `range` revisited

we can use a variable in a `for ... in range(...):`

``````for i in range(5):
...``````

is equivalent to:

``````i = 0
for _ in range(5):
...
i = i + 1``````

In other words, `i` takes on the values from 0 up to but not including 5.

We can also tell the range to begin somewhere other than 0:

``````for i in range(start, stop):
...``````

is equivalent to:

``````i = start
for _ in range(stop - start):   # start - start is how many times to repeat the loop
...
i = i + 1``````

Remember `range(start, stop)` means begin at `start`, go up by one each time, but for the loop to end just before we get to `stop`:

``````>>> for x in range(3, 7):
print(x)

3
4
5
6``````

So here is the quiz code renamed:

``````def count_factors(m):
n = 0
d = 1
for _ in range(m):
if m % d == 0:
n = n + 1
d = d + 1
return n``````

This leverages range variables and start/stop version or range:

``````def count_factors(m):
n = 2   # because 1 and m will always be factors
for d in range(2, m):   # start at 2, go up to, but do not include m itself
if m % d == 0:
n = n + 1
return n``````

This corrects above so that it works when `m` is 1:

``````def count_factors(m):
n = 1   # because 1 and m will always be factors
for d in range(2, m+1):   # start at 2, go up to and *include* m itself
if m % d == 0:
n = n + 1
return n``````

#### nested `if` statements to choose between more than two options

``````def calculator(x, y, op):
if op == '+':
z = x + y
else:
if op == '-':
z = x - y
else:   # assuming anything else for op means "multiplication"
z = x * y
return z``````

In this way we can select from one of three (or more) cases. It assumes that `op` is either `'+'`, `'-'` or `'*'` but it behaves so that anything other than `'+'` or `'-'` leads to multiplication.

Here is a further nested version to handle a fourth and final “error” case; it assumes `op` is `'+'`, `'*'`, `'-'`, but, if not, it returns `-1` to signal an error. (In class, we discussed how the problem with using a string result like `'error'` instead of a number such as `-1` is that it can lead to confusion over the type of the variable `z` - we would like to abide by the idea that within a single function a variable is assigned one and only one type. But returning `-1` or any number is a bad idea. Later in the semester - time permitting - we will say a superior approach to error handling.)

``````def calculator(x, y, op):
if op == '+':
z = x + y
else:
if op == '-':
z = x - y
else:
if op == '*':
z = x * y
else: # if not any of +, - or * then indicate an error
z = -1
return z``````

With each additional case we wish to add, we need to indent further. That cascading indentation is sometimes referred to as “the pyramid of doom” - it makes code quite difficult to read! Luckily, there is a better way.

Before we get to it, let’s consider what happens if we just use separate `if` statements:

``````def calculator(x, y, op):
if op == '+':
z = x + y
if op == '-':
z = x - y
if op == '*':
z = x * y
else: # if not any of +, - or * then indicate an error
z = -1
return z``````

This looks cleaner - no pyramid of doom! - and it is fairly easy for the reader of the code to get the gist. However, it is wrong! If `op` is anything other than `'*'` then the final `else` branch is executed and the result is -1:

``````>>> calculator(9, 4, '*')   # this is fine!
36

>>> calculator(9, 4, '+')   # watch out!
-1``````

#### multiway`if` statement

Instead, we introduce the `elif` reserved word that is used to make a multiway conditional statement:

``````def calculator(x, y, op):
if op == '+':
z = x + y
elif op == '-':
z = x - y
elif op == '*':
z = x * y
else: # if not any of +, - or * then indicate an error
z = -1
return z``````

This has the benefit of making the code easy to read (each case is at the same level of indentation), is correct (vastly more important!), and (since we are impatient computer scientists), once one of the `if`/`elif` test expressions evaluates to `True` and its corresponding branch is executed, the program skips to just past the last `elif`/`else` clause of the multiway branch.

#### Combining Boolean expressions with `and`, `or,`not`

Suppose we wish to write a function that takes a string as input and returns a similar string except all but the uppercase letters filtered out, like this:

``````>>> uppers_only('Computers Only Offer Loops!')
'COOL'``````

Here is a way to do that with what we have studied so far:

``````def uppers_only(s):
t = ''
for c in s:
if ord(c) >= ord('A'):
if ord(c) <= ord('Z'):
t = t + c
return t``````

Makes sense. But seems complicated. The nested if is unsatisfying since it we are trying to convey that a character’s ASCII code is in a range (any of the codes that correspond to uppercase letters). Not surprisingly we can do better, by using a Boolean connective in this case `and`:

``          ord(c) >= ord ('A') and ord(c) <= ord('Z')``

Before we rewrite our function, let’s step back and introduce the three standard Boolean connectives in Python. (Bear in mind that in Python they are conveniently named, not so in many other PLs.)

We have already seen the “not equal” operator as in:

``   <exp> != <exp>``

that is really shorthand for:

``   not (<exp> == <exp>)       # parentheses not necessary - included here for clarity``

`not` is a reserved word in Python that operates on an expression to its right that is expected have been evaluated to a Boolean value and then it inverts the value:

``````>>> not False
True
>>> not True
false``````

`and` is a reserved word in Python that operates on two Boolean expressions, one to its left and one to its right. (So `and` is much like binary arithmetic operators, but it takes two Booleans and produces a Boolean.) Here is the “truth table” for `and`:

``````>>> False and False
False
>>> False and True
False
>>> True and False
False
>>> True and True
True``````

In other words (and not surprisingly!) the “and” of two Boolean values is true only if both its operands are true.

`or` behaves comparably: the “or” of two Boolean values is true if either of its operands are true:

``````>>> False or False
False
>>> False or True
True
>>> True or False
True
>>> True or True
True``````

We can use `and` to encode the idea of testing if a number is within a range of values:

``````def between(low, x, high):
b = low <= x and x < high
# Python allows low <= x < high  <--- BAD IDEA! do not use in our class!
return b``````

earlier we used ASCII codes to check if one character is “less than” another:

``````def char_lt(c, d):
b = ord(c) < ord(d)
return b``````

Python does that automatically:

``   'x' < 'y'``

is translated (“under the hood”) to:

``   ord('x') < ord ('y')``

so we can use `<`, `<=`, `>`, `>=` directly on characters (and, a little later, we will see that we can use them on strings more generally):

``````def is_upper_char(ch):
b = len(ch) == 1 and 'A' <= ch and ch <= 'Z'
return b``````

Observe that we check that `ch` is a string of length 1.