Class 10: Conditionals

Objectives for today

Booleans and Boolean operators

Last time we introduced Boolean types, specifically the bool type in Python.

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

bools have only two values: True and False. As their name suggests they are used to express what is true and what isn’t.

All Boolean operations 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
False
>>> not False
True
>>> True and False
False
>>> True or False
True

Boolean operators are often expressed as truth tables, e.g. a <operator> b:

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.

PI Questions (Boolean operators)1

Relational operators

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

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

Recall that in Python (unlike math), = is assignment, and == is equality (with != implementing “not equals”).

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

PI Questions (relational operators)1

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.

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

PI Questions (conditional statements)1

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 condition expression.

Example: Monte Carlo sampling

We can calculate π via simulation. Consider a quarter circle inscribed inside a square of side 1 (i.e. with an area of 1). If we randomly select points inside the square, approximately a π/4 fraction of those points should be inside the 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.

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)
3.2
>>> calculate_pi(100)
3.08
>>> calculate_pi(1000)
3.088
>>> calculate_pi(10000)
3.1604
>>> calculate_pi(100000)
3.14324
>>> calculate_pi(1000000)
3.141572
>>> calculate_pi(10000000)
3.1411364

Show a potential implementation…

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