Lecture 7: Conditionals

Objectives for today

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')
True
>>> s.startswith('b')
False

The returned values here, True and False, are values of a new type we haven’t yet encountered, 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.

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.

Peer instruction questions (Boolean operators) [1] (Section A, Section B)

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.,

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

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

Peer instruction questions (Relational operators) [1] (Section A, Section B)

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

Peer instruction questions (Conditional statements) [1] (Section A, Section B)

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 cognitive burden of reading code. When returning boolean values from within an if-else statement we first need to 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.

Here is another example of using conditionals. We can calculate pi 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 pi/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 Pi. This approach is called Monte Carlo sampling.

import math, random

def calculate_pi(num_samples):
    """
    Approximates pi via Monte Carlo sampling.
    
    Generates n random points in a 2x2 square centered at (0,0)
    and then checks what percentage of those points are within
    the unit circle also centered at (0,0).

    Args:
        num_samples: number of Monte Carlo samples

    Returns:
        approximation to pi
    """

    in_circle = 0

    for i in range(num_samples):
        # Generate random "dart" inside 2x2 square
        x = random.uniform(-1, 1)
        y = random.uniform(-1, 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

    # Determine ratio of "darts" inside circle vs total samples
    percent_in = in_circle / num_samples

    # Approximation pi is the ratio times total area of the square
    return 4 * percent_in
>>> calculate_pi(10)
2.8
>>> calculate_pi(100)
3.36
>>> calculate_pi(1000)
3.156
>>> calculate_pi(10000)
3.1504
>>> calculate_pi(100000)
3.13808
>>> calculate_pi(1000000)
3.144316
>>> calculate_pi(10000000)
3.1415884

Summary

  1. Boolean values and operators
  2. Relational operators
  3. Conditional statements

Complete Lab 3 for Tuesday night, work on Practice Problems 4, read through Prelab 4 and Lab 4.