Session 8

Working with Control Statements

CS 1510
Introduction to Computing

Today's Review

The lab went a bit smoother yesterday. As a result, most of the questions you asked were about the important ideas of the assignment, not background knowledge. The only exception was the Python 2/3 glitch, which caused a few arithmetic headaches. Now, we can all know ways in which Python 3 was a step forward...

Lessons from Task 1

Filling the blank in this program required you to write one arithmetic statement and one if statement. The arithmetic was a weighted sum of four grades:

    total_score = 0.15 * lab_score  + 0.35 * programs_score + \
                  0.30 * exam_score + 0.20 * final_score

Because the weights add up to 1, the resulting sum is the answer. If the weights added up to anything else, we would need to divide the weighted sum by the sum of the weights.

Next came the if statement. Its job is to put the grade, a number between 0 and 100, into one of five bins according to the course grading scale. With elif clauses, that's relatively straightforward:

    if total_score >= 90:
        grade = 'A'
    elif total_score >= 80 and total_score < 90:
        grade = 'B'
    elif total_score >= 70 and total_score < 80:
        grade = 'C'
    elif total_score >= 60 and total_score < 70:
        grade = 'D'
        grade = 'F'

But we can do better. Notice how the else clause has no boolean expression on it. That's because, if the program ever gets that far, we know that the score is less than 60. How do we know that, because all the preceding tests failed!

We can take advantage of the same reasoning when we write our elif clauses. If we ever get to this part of the statement:

    elif total_score >= 80 and total_score < 90:

... what do we know? The score is less than 90. If it were 90 or more, then the first test would have passed, letter_grade would have been set to 'A', and program would have continued with the statement after the if.

So, if we know the score is less than 90, we don't need to check to see if it is true:

    elif total_score >= 80:

We can use the same logic for the other two elif clauses and simplify our statement to this:

    if total_score >= 90:
        grade = 'A'
    elif total_score >= 80:
        grade = 'B'
    elif total_score >= 70:
        grade = 'C'
    elif total_score >= 60:
        grade = 'D'
        grade = 'F'

This is another common pattern you will see, called a range test. If we put the ranges in a logical order, we can often let the boolean expressions cascade forward, testing only the unknown half of the range.

One student asked, Is there any way to eliminate the repetition in the 'if' statement? To an experienced programmer and an intro course professor, this is a beautiful question. The answer is "Yes!" The simplest way requires a bit of Python we haven't studied yet. We will in a few weeks.

(I once ran a workshop...)

Another student asked whether we should name the weights we use, such as:

    lab_score_wt      = 0.15
    programs_score_wt = 0.35
    exam_score_wt     = 0.30
    final_score_wt    = 0.20

Again, this is a beautiful question. If I come back to this program later, I will have to search for the weights in the middle of the program, despite the fact that they are the part of the program I am most likely to change next semester. So, yes, this is a good programming practice.

However, these names are not really variables. Their values never change during program execution. They are constants. A common practice in Python, one mentioned in the course programming standards, is to use ALL CAPS for the names of constants:

    LAB_SCORE_WT      = 0.15
    EXAM_SCORE_WT     = 0.30
    FINAL_SCORE_WT    = 0.20

This makes the program easier to reuse later. And in this case, we probably will reuse it... The code from this lab comprises two parts of a completing grading program for the course.

(I don't expect you to go all the way on niceties such as this in the lab, because the key then is to execute experimentations and solve problems with a specific goal in mind.)

Here is a working solution, without named constants.

Lessons from Task 2

Debugging this program required you to find three errors:

  1. input() gives us a string, not a number. We must convert the value before adding.

  2. // performs integer division, giving us an integer quotient, not a real number. We must use / instead.

  3. (the big one) A range() includes its lower bound, but not its upper bound. We must widen this range by 1 to make the program process all of the programs.

Using Python 2 threw a wrench into the works.

The most important bug, though, shows up in every Python. One option is to lengthen the range on the high end:

    for program_number in range(1, number_of_programs+1):

Another option is to lengthen the range on the low end:

    for program_number in range(0, number_of_programs):

What are the advantages and disadvantages of each? Discuss. Neither is ideal. Trade-offs.

As strange as this feature of range() may seem, you will soon see some occasions in which it feels just right. Many programming operations work more smoothly when we cound from 0 and use a < test to create an open right end of a range...

Floating-point values...

In any case, in Python 3, this program does the job.

Lessons from Task 3

I asked you to fill the blank in this program using a particular kind of while loop:

  1. Read a value.
  2. While the value is not the sentinel value,

All of you tried to write code to perform of the task. Many of you were a bit sloppy, though. Some swapped the order of the steps inside the loop. After doing that, a few of you decided that you didn't need the step before the loop. This might leave you with this:

    score_str  = input("Enter program score: ")     # these may be
    prog_score = int(score_str)                     # disappeared

    while prog_score != -1:
        score_str  = input("Enter program score: ")
        prog_score = int(score_str)
        total_points += prog_score
        number_of_programs += 1

What sort of problems did you have? ... including the -1 in the total score. ... off by one program.

The purpose of a sentinel value is to say: There are no more values to process. It's not one of the values; we don't want to add it to our running total, or count it as a value. That means the test Is it the sentinel? must happen in between reading a value and processing a value.

Take anothe look at the algorithm written in English. That's just what happens. The test always happens between the step that reads a value and the step that processes a value. It happens with for the first read step, which is outside the loop. It also happens with every future read step, which is at the bottom of the loop. The next thing that happens after that step is ... the test.

As a couple of you said in lab and in your responses, Order matters. Yes! When we see or design an algorithm, we should be as careful as possible to translate it faithfully into code. The re-ordered example above is not a faithful translation, and the resulting program behaves differently than the algorithm intends.

This is a more faithful translation:

    score_str  = input("Enter program score: ")
    prog_score = int(score_str)

    while prog_score != -1:
        total_points += prog_score
        number_of_programs += 1
        score_str  = input("Enter program score: ")
        prog_score = int(score_str)

The sentinel loop is another common pattern you will see in programs. It is a handy solution to a common problem: we want to process a list of items, but we don't know at the beginning how many items there are. As long as there is a sentinel that we can watch for, we are fine.

We might ask a question about this code similar to beautiful question asked by a student about our letter-grade if: Is there any way to eliminate the repetition? This loop repeats the two lines for reading a value. Indeed, so does the English language description of the algorithm!

The answer is again yes, but this time we don't need any Python knowledge beyond Chapter 2. But we do need to write a new kind of loop. It looks like it never ends, but it does...

    while True:
        score_str  = input("Enter program score: ")
        prog_score = int(score_str)
        if prog_score == -1:
        total_points += prog_score
        number_of_programs += 1

Think this through...

Notice that the test has been inverted. The previous version checks to see if the value is not the sentinel, so that it can continue. This version checks to see if the value is the sentinel, so that it can stop.

Notice, too, that we are implementing a slightly different algorithm:

  1. Read a value.
  2. If the value is the sentinel value, stop.
  3. Process the value.
  4. Do it again.

What are the advantages and disadvantages of each? Discuss. While neither may be perfect, the new version seems like a winner. This is closer to how a non-programmer would describe the same task, and the code is easier to read even for a programmer.

You will probably see this idea in many programs, too. It is called a loop and a half. Can you see why?

A sentinel loop is an idea, an alternative to a fixed-count for-loop. We can implement this idea in many ways. Today, we saw two: a traditional inverted loop and a loop-and-a-half.

This is one of the Big Ideas of Computer Science: Any idea can be implemented in many different ways. This will bedevil you sometimes, especially when you are first learning to program. In the end, though it is a great source of power.

Here are the complete programs one using a standard sentinel loop and one using a loop and a half.

Homework 3

Do you have any questions about Homework 3?

Wrap Up

Eugene Wallingford ..... ..... September 18, 2014