Class 7

Conditionals

Objectives for today

  • Define a boolean type and enumerate its possible values
  • Use relational operators to compare values
  • Explain and use AND, OR and NOT operators
  • Generate the truth table from simple boolean expression and implement a truth table as a boolean expression
  • Explain when conditional statements are used
  • Describe the evaluation order of if-elif-else statements in Python
  • Use conditional statements in a Python program

Booleans and Boolean operators

Earlier we saw that strings have a method startswith. We would guess that method will indicate if the string starts with the supplied parameter:

s = "hello"
s.startswith('h')
s.startswith('b')
True
False

What are True and False. They are not strings because they’re not enclosed in quotes, nor are they integers or floating points numbers. They are bools, which is short for Boolean. Booleans are a type that can only take on two values True and False. Booleans and Boolean logic are a key programming tool, particularly for decision-making (i.e., do we want to do a particular operation or not).

All Boolean operations (i.e., computations with boolean inputs and a boolean output) can be implemented with a combination of the basic boolean operations AND, OR and NOT. Or in Python and, or, and not. The first two are binary operations, i.e. have two inputs, the last is unary, i.e. has a single input.

not True
not False
True and False
True or False
False
True
False
True

Boolean operators are often expressed as truth tables, e.g. we would express the truth tables for a and b and a or b as:

a b or and
True True True True
True False True False
False True True False
False False False False

Much like arithmetic operators, boolean operators have precedence: not, then and, then or.

Relational operators

We previously saw string methods that return bool. Another common way to generate bools is with relational operators applied to numerical (and other) inputs, e.g.

<, >, <=, >=, ==, !=

These mostly implement familiar relationships like greater than (\(\gt\)) or greater than or equal (\(\ge\)). But recall that in Python (unlike math), = is assignment. Thus equality is implemented with == (and != implementing “not equals”).

1 < 2
2 > 1
2 != 1
2 == 2
2 == 2.0
True
True
True
True
True
x = 2
1 < x < 3
True
s == "hello"
s[1] == "h"
s[0] == "h"
s[:2] == "he"
True
False
True
True

Making decisions

One of the key uses for bools is making decisions. So far none of our programs have made any choices. Being able to do so is very powerful. The general pattern for conditional statements in Python:

if (boolean expression A):
    statement1
    statement2
elif (boolean expression B):
    statement3
    statement4
else:
    statement5
    statement6

statement7
statement8

If A evaluates to True which statements will be executed? Statements 1,2,7,8. What if A evaluates to False? If B evaluates to True, statements 3,4,7,8. If neither A or B evaluate to True, then statements 5,6,7,8 will execute.

Only one of the if, elif or else is “selected” and its body executed, even if multiple of the boolean expressions would have evaluated to true, and that selection occurs in order (i.e., the first “true” branch is selected).

Note elif and else are optional and no branch of the conditional needs to be selected. Multiple elifs are permitted.

Some other examples:

def is_odd(n):
    if n % 2 == 1:
        return True
    else:
        return False

def positivity(n):
    if n == 0:
        print(n, "is zero")
    elif n > 0:
        print(n, "is positive")
    else:
        print(n, "is negative")

A note about coding “best practices”. Can we write is_odd more concisely? Yes. Almost anytime we are returning a boolean from a conditional statement we can do so more concisely, e.g.,

def is_odd(n):
    return (n % 2) == 1

Why is this better style? Recall that good coding style is often about minimizing the cognitive burden of reading code. When returning boolean values from within in if-else statement we first need understand the condition expression and then map it to separate, possibly different return values. That is additional cognitive burden compared to just needing to understand the relational expression.

Example: Monte Carlo sampling

One of the ways to calculate π is via simulation. Consider a quarter circle inscribed inside a square of side 1 (i.e. with an area of 1). The area of the quarter circle is \(\frac{\pi 1^2}{4}\) and thus ratio of that area to that of the square is π/4. If we randomly select points inside the square, approximately a π/4 fraction of those points should be inside the quarter circle (imagine randomly throwing darts at square with an inscribed quarter circle). By calculating the ratio of randomly sampled points inside the circle to the total number of sample, we can estimate the value of π.

This approach is called Monte Carlo sampling, and it is widely used in scientific applications when a problem is too hard to solve deterministically, but we can estimate a solution in reasonable time using randomness. For example, in CS321 “Bioinformatics Algorithms” we use randomized algorithms to predict where regulatory proteins bind to DNA (transcription factor binding sites).

Let’s implement a function calculate_pi with a single parameter, the number of sampled points. What should we expect as we increase the number of samples? We will get closer and closer (probabilistically) to the actual value of pi. For example:

calculate_pi(10)
calculate_pi(100)
calculate_pi(1000)
calculate_pi(10000)
calculate_pi(100000)
3.6
2.8
3.22
3.1392
3.14232
import math, random

def calculate_pi(num_samples):
    """
    Approximate pi via Monte Carlo sampling
        
    Args:
        num_samples: number of Monte Carlo samples

    Returns:
        Approximate value of pi
    """
    in_circle = 0
    
    for i in range(num_samples):
        # Generate random "dart" inside unit square
        x = random.uniform(0, 1)
        y = random.uniform(0, 1)
        
        # Determine distance to origin
        dist = math.sqrt(x*x + y*y)

        # Count number of darts inside the circle
        if dist <= 1:
            in_circle += 1 # equivalent to in_circle = in_circle + 1
            
    # Calculate pi based on ratio of "darts" inside circle vs total samples
    return (4 * in_circle) / num_samples