COMP1701-004

fall 2023

lec-14

Lo, Your Month

Any A3 Questions?

I will only mark the 2 files that were included in the starter code.

RECALL

Let's do a brain dump: write down everything you can remember - words, phrases, pictures, whatever - about things we covered in lecture and lab last week.

You've got 1 minute.

let's talk about these  things today:

 counted loops

 sentinel loops

loopsies

categorizing loops

The Python syntax allows for 2 types of loops.

  1. while loops
  2. for loops (covered later)

Some textbooks, sites, and instructors suggest categorizing while loops...but agreement on the definition of these further categorizations isn't always great.

This can be pretty confusing - especially when you're first learning this stuff!

Nevertheless, since our department uses certain terms in certain ways, it's best if I bring them up, since you'll be encountering them over your stay here.

counted loops

counted loops

A counted/counting/counter-controlled loop is a loop that runs a known number of times.

This kind of loop may also be called a definite loop.

counted loops

2 common flavours

Flavour 1

Do something a specific number of times.

counter = 0
DESIRED_COUNT = 4

while counter != DESIRED_COUNT:
    # do something
    print('something')

    counter += 1

Flavour 2

Do something until some upper/lower bound is reached (or exceeded).

counter = int(input("times to loop? "))

while counter != 0:
    # do something
    print('something')

    counter -= 1

counting UP version

counting DOWN version

curr_val = int(input("Yer number? "))
UPPER_BOUND = 14

while curr_val < UPPER_BOUND:
    # do something
    print('something')

    curr_val += 2

counting UP by 2's version

curr_val = int(input("Yer number? "))
LOWER_BOUND = 2

while curr_val > LOWER_BOUND:
    # do something
    print('something')

    curr_val -= 3

counting DOWN by 3's

Flavour 1 is just a specific case of flavour 2!

We could also use <0 here.
In fact, it might even be a better idea?....

🙋🏻‍♂️❓🙋🏻‍♀️While we're here...can you identify the 6 required parts of any while loop here?

counted loops

def sing_that_song():

    number_of_bottles = 99

    while number_of_bottles > 2:
        print(f"{number_of_bottles} bottles of age-appropriate beverages on the wall,")
        print(f"{number_of_bottles} bottles of age-appropriate beverages,")
        print("take one down, pass it around,")
        number_of_bottles = number_of_bottles - 1
        print(f"{number_of_bottles} bottles of age-appropriate beverages on the wall,")
        print()

    # other stuffs here


sing_that_song()

Questions

  1. What's our LCV?
  2. What is the purpose of that 2 in the condition?
  3. ***How many times will this loop loop?  (consider temporarily using a smaller number than 99 to help you here)
  4. What happens if we use number_of_bottles >= 3?
  5. How can we rewrite the line that modifies the LCV - and what's that op called?

Remember those bevs? Counted loop for sure!

HONOURS_CUTOFF = 3.6
PASS_CUTOFF = 2.0


def display_grade_stats(num_students: int) -> None:

    student_count = 0
    num_honours = 0
    num_passed = 0
    num_failed = 0

    while student_count < num_students:
        gpa = float(input(f"Student {student_count + 1} GPA: "))

        if gpa >= HONOURS_CUTOFF:
            num_honours += 1
        
        if gpa >= PASS_CUTOFF:
            num_passed += 1
        else:
            num_failed += 1

        student_count += 1

    print(num_honours, num_passed, num_failed)

counted loops

The bounds on your counted loop do NOT have to be hard-coded in the code itself!

Questions

  1. What's the LCV here? Then what's the other variable in the condition?
  2. How many times does this loop loop?

To be called a counted loop, you only need to be able to figure out how many times the loop will loop when you RUN the code, not when you DESIGN it.

counted loops

from random import randint


def dice_sum(num_dice: int) -> int:
    sum = 0
    dice_rolled = 0

    while dice_rolled < num_dice:
        sum += randint(1, 6)

    return sum

Another one....

Question

  1. What's the LCV here?
  2. How many times does this loop loop?
  3. What's a quick way of figuring out the number of times a counted loop loops?
  4. If we change the second argument to randint to a 1, what is returned if we call dice_sum(0)? TRACE IT
  5. Same as 4, but this time calling dice_sum(2)? TRACE IT

counted loops

from random import randint


def dice_sum(num_dice: int) -> int:
    sum = 0
    dice_rolled = 0

    while dice_rolled < num_dice:
        sum += randint(1, 6)
        dice_rolled += 1

    return sum

BTW - did you see the bug?

#$%!@

from random import randint


def dice_sum(num_dice: int) -> int:
    sum = 0
    dice_rolled = 0

    while dice_rolled < num_dice:
        sum += randint(1, 6)
            

    return sum

This error creates an infinite loop.

🙋🏻‍♂️❓🙋🏻‍♀️Which of the 3 categories of bug is this?

counted loops

These counted loops are all well and good...

We need a different solution!

...but it's more common to bump into situations where we can't know how many times we need to loop.

sentinel loops

sentinel loops

For Your Consideration

You've just attached an altimeter to a little birdie. (Go science!)


You plan on releasing said bird into the wild, letting it fly around for a few days, and then capturing it - and the altimeter.

 

You then want to grab the data off the altimeter so you can get a feel for the different altitudes the feathered fellow flew.

Say that the altimeter records integers representing the altitude (rounded to the nearest whole meter) at fixed intervals of one minute and marks the end of recording with an altitude of -1.

TASK

Write a program that echoes the data in the altimeter's memory to the console in minute : altitude format .
Assume you have a function called next_altitude() which gets the next altitude out of the altimeter's memory.

sentinel loops

As an example, if this is what's in the altimeter's memory...

0
5
4
0
10
0
-1

...then this is what we'd want our code to print to the console:

1:0
2:5
3:4
4:0
5:10
6:0

minute #s

altitudes

altitudes

We don't know in advance how many readings there are going to be, so a definite loop won't cut it.

We're going to need an indefinite loop!

sentinel loops

altitude = next_altitude()
curr_minute = 1

while altitude != -1:
    print(f"{curr_minute}:{altitude}")
    curr_minute += 1
    altitude = next_altitude()

one possible solution

A condition determines what keeps the loop going.

The condition uses a variable - the LCV (loop control variable).

The LCV is given a useful initial value.

The LCV is changed inside the loop.

One or more statements are run while the loop is going. (the loop body)

This has everything we come to expect from a good loop:

What's this step called again?

sentinel loops

This kind of loop is so common that it gets its own name!

altitude = next_altitude()
curr_minute = 0

while altitude != -1:
    print(f"{curr_minute}:{altitude}")
    curr_minute += 1
    altitude = next_altitude()

A sentinel loop is a loop that keeps going until it reaches a special value - the sentinel value.

The textbook calls this a listener loop.

sentinel loops

In this case, the sentinel value is -1

altitude = next_altitude()
curr_minute = 0

while altitude != -1:
    print(f"{curr_minute}:{altitude}")
    curr_minute += 1
    altitude = next_altitude()

As a programmer, you want to choose a value that is guaranteed NOT to be one you'd expect in the data you're looping through!

Here, we'd never expect to have a negative value for altitude;
we could choose any negative value (like -999), but it's a good idea to keep things simple!

sentinel loops

What would you choose as a sentinel value for...

  1. test scores?
  2. first names?
  3. decibel readings?
  4. temperatures?
  5. precipitation levels?
  6. incomes?

Choosing a reasonable sentinel value isn't always as easy as it seems at first glance - it requires some knowledge about the problem domain!

sentinel loops

There are a 3 things I'd like to call out here:

altitude = next_altitude()
curr_minute = 0

while altitude != -1:
    print(f"{curr_minute}:{altitude}")
    curr_minute += 1
    altitude = next_altitude()
  1. Is the sentinel ever used inside the loop itself? 
  2. How well does this handle the case when there are NO altitudes? Trace it!
  3. This pattern - priming read, test for sentinel, re-prime - is something you will see repeatedly throughout your studies and your career - it's very useful to memorize it!

loopsies

loopsies

Loops can be a bit slippery at times!

Let's go over examples of some loops with some common bugs.

I sometimes call these bugs "loopsies", because my brain's an idiot that thinks its clever.

loopsies

Solid tracing skills - whether visually, on paper, or via a debugger - are essential for debugging loops.

You should look at every opportunity to trace something as a workout and not a chore.

loopsies

LETTER_LIMIT = 3

print("Enter a 3-letter word, one letter per line.")
num_letters_read = 0
num_lowercase_read = 0

while num_letters_read != LETTER_LIMIT:
    letter = input()
    if letter.islower():
        num_lowercase_read += 1

print("There were", num_lowercase_read, "letters in that word.")

This Code SHOULD:
Read in 3 letters, then tell you how many of them were lowercase.

There is ONE loopsie in this code.
What is it? Trace to find it.
How would you fix this bug?

Loopsie: forgot to update the LCV every time through the loop. Super-common bug!

oops!

loopsies

dial_reading = int(input("dial reading? "))

while dial_reading < 0 and dial_reading > 10:
    dial_reading = int(input("dial reading? "))

print("The dial is set to", dial_reading)

This Code SHOULD:
Validate input so the dial reading is in [0, 10].

There is ONE loopsie in this code.
What is it? Trace to find it.
How would you fix this bug?

Loopsie: that loop condition isn't what you think it is...

Pro Tip:
When dealing with compound loop conditions, it is often easier to think of a stopping condition and then "flip" it logically to come up with the loop condition!

loopsies

def damage_from_enemy():
    return 3


def fight_unbeatable_foe(hit_points):

    while hit_points != 0:
        print(f"Ow! {hit_points} hp left!")
        hit_points -= damage_from_enemy()


fight_unbeatable_foe(4)

This Code SHOULD:
Stop after the player is dead (i.e. reaches 0 hit points)

There are TWO loopsies in this code.
What are they? Trace to find it.
How would you fix these bugs?

Loopsie: that loop condition is too picky!

Loopsie: that loop condition is too picky!

Loopsie: that print happens too soon!

loopsies

low_bound = int(input("lower bound? "))
high_bound = int(input("upper bound? "))

curr_num = low_bound

num_odds = 0

while curr_num < high_bound:
    curr_num += 1
    if curr_num % 2 != 0:
        num_odds += 1


print(f"# odds in [{low_bound},{high_bound}]: {num_odds}")

This Code SHOULD:
Find all odd numbers between 2 user-defined numbers, inclusive.

Loopsie #1: counted loop condition is off-by-one

Loopsie #2: LCV is changing too soon

There is TWO loopsies in this code.
What is it? Trace to find it.
How would you fix this bug?

Made with Slides.com