Combining Selection
Learning Objectives
At the end of this sub-unit, students should
- know how to combine selection statement.
- know how to flatten selection statement if needed.
Multiple Selections
Recap that the syntax for if-statement always starts with the keyword if
which can then be followed by elif
and else
.
If we then have another if
keyword appearing, this will constitutes a separate if-statement.
For instance, in the example below, we have two if-statements.
One way to determine which if-statement a particular elif
or else
belongs to is by going up the code and find the nearest if
keyword with the same indentation level.
This way, we can see that the else
keyword is attached to the second if
keyword and the elif
keyword is attached to the first if
keyword.
This will affect the execution of the code. As we have discussed in the previous unit, we have the following property.
At most one of the blocks will be executed.
Since there are two if-statement in the example, there will be at most two blocks executed instead of one. Additionally, note that Line 5 will always be executed because it is the beginning of the second if-statement. An exception to this is of course when the program terminates due to an error before reaching Line 5.
What is the implication of this?
We can actually rewrite the letter grade problem differently.
First, note that the disjoint version can be easily rewritten by replacing all elif
into if
because a single value cannot satisfy more than one condition.
Second, to highlight the differences more clearly, we will rewrite the overlapping version as follows.
In the next solution, we will use multiple if-statement instead of a single if-statement with multiple elif
keywords.
We will refer to this version as multi-if version.
Incorrect Letter Grade
Note that simply changing all the elif
keywords into if
keywords in the overlapping version will give us a wrong solution.
Try running the following code with mark = 87
to see the problem.
Letter Grade
To see why this code is correct, we can look at one of the letter grade.
Say we look at grade = "C"
.
We need to ask ourselves, what conditions will make the value of the variable grade
remains C at Line 10?
- It must be assigned
"C"
at Line 5.- Hence, it must satisfy the condition
mark >= 70
.
- Hence, it must satisfy the condition
- It must NOT be assigned
"B"
at Line 7.- Hence, it must not satisfy the condition
mark >= 80
.
- Hence, it must not satisfy the condition
- It must NOT be assigned
"A"
at Line 9.- Hence, it must not satisfy the condition
mark >= 90
.
- Hence, it must not satisfy the condition
The solution to that implies mark
is in the range of [70, 80)
.
This corresponds exactly to the requirement.
Let us compare this solution with the previous solution.
One interesting difference is the order in which the letter grade appears in the code.
Using the overlapping version, we can see that "A"
appears first whereas in the multi-if version, "F"
appears first.
Another interesting property is that in the overlapping version, the variable grade
will only ever be assigned a value once.
You can try to trace the execution to see this fact.
It can also be deduced from the use of if
, elif
, and else
.
Due to the following property of if-statement.
At most one of the blocks will be executed.
We can infer that only one of the assignment block will be executed.
Now, in the multi-if version, the variable grade
is potentially being assigned 5 times.
Each of the assignment block may be executed.
For instance, when mark
is 99.
In fact, the flowchart highlights this behavior properly as you can see below.
The dashed path highlighted in blue is the path taken when mark
is 77.
Notice how there are 3 assignments to grade
.
Alternatively, we can use the arrow on the code to show which lines are executed again. Compare the number of lines executed with the overlapping version.
So that is how multiple if-statements can be combined to perform more checks. This is actually not something new. If we can actually write the entire if-statement inside the rectangle as shown above, then it is really just a sequence of if-statements.
This is an example of the idea of composition in programming. One concept can actually be combined (i.e., be composed) with other concepts. The interactions between the concepts can be confusing. As such, we should try to use the concepts in the intended way and not mix concepts without knowing all the interactions. We are back to our motto of writing readable code.
Creation of Multiple Selections
One way to think about multiple selections is to first start with sequence and then expand or replace the block with the flowchart for if-statement.
Inner Selections
There is another way of composing the selection. We have discussed how selection can be sequenced. But note that if-statement is also a statement and a group of 1 or more statement is called a block. Since we can put a block inside an if-statement, we can also put another if-statement inside another if-statement.
How useful is this composition? Consider the case of determining if a year is a leap year or not. According to Wikipedia, we have the following definition.
Leap Year
Every year that is exactly divisible by four is a leap year, except for years that are exactly divisible by 100, but these centurial years are leap years if they are exactly divisible by 400. For example, the years 1700, 1800, and 1900 are not leap years, but the years 1600 and 2000 are.
Let us look at the definition more closely. We have three divisibility checks that we need to do.
- Divisible by 4.
- Divisible by 100.
- Divisible by 400.
We know how to check for divisibility.
The variable year
is divisible by a value val
if the following expression year % val == 0
evaluates to True
.
More interestingly, we need to know how is the check actually done. The keyword except here implies that after we check if a year is divisible by 4, we still need to check if the year is also divisible by 100. Even worse, the phrase "... but these centurial years are leap years if they are exactly divisible by 400." implies that we then need to check if the year is divisible by 400 again. Rearranging this, we get the following hierarchy shown below. The hierarchy closely match the code!
- Is year divisible by 4?
- Is year divisible by 100?
- Is year divisible by 400?
- Leap year
- Otherwise
- Not leap year
- Is year divisible by 400?
- Otherwise
- Leap year
- Is year divisible by 100?
- Otherwise
- Not leap year
The analysis tab shows the conditions that are satisfied at each print
.
You should convince yourself that the code is correct so it is a start.
However, readability may be a little compromised with the deep nesting.
At line 5 and 7, we indent the line 3 times.
The deeper the nesting, the harder it is to read given the limited width of the monitor and/or paper we have to work with.
Flattening
Often, we want to avoid such a deep nesting.
The analysis of the conditions can actually help us.
By definition, the conditions are all disjoint.
This is achieved by negating the condition on the else
branch.
Negating the condition is indicated by the comment # NOT
.
As a code, this can be done by the not
operator.
More precisely, the conditions at each nesting is indicated below.
Nesting Condition Analysis
This analysis match exactly our analysis for the leap year problem above.
So this at least give us confidence that we have not made major mistakes.
But more interestingly, how do we convert this to a non-nested if-statement (a.k.a. flattened).
The idea is to look at each blocks independently and wrap each inside an if-statement.
We may also use elif
keyword for the next block, but not exactly necessary because all the conditions are disjoint if we do this conversion correctly.
After we identify the blocks and wrap them inside an if-statement, we put the analyzed condition including the and
operator.
This gives us the following code.
In the code below, we use elif
as a good practice so that we do not evaluate the next condition if the previous condition is already satisfied.
Note that we will still convert all the else
keywords because there is a possibility some cases are not covered.
However, based on the analysis, we know these conditions must be satisfied.
Flattened If-Statement
Flattened Leap Year
So let us apply this technique to the leap year problem. We will do this in the systematic way as above. Try to do it on your own first before looking at the code below.
Flattened Leap Year
In this case, we can actually use else
for the last one because we can analyze that all cases are covered.
There is yet another way to flattened this.
To arrive at this way of flattening, we need to look at the 4 print
s very closely.
We can identify that there are duplicates.
In fact, we only have 2 different print
s.
However, these 2 print
s are separated by two different conditions.
With some thinking --and by saying the condition in English-- we can say that a particular year is a leap year if "condition at Line 1 is true" OR"condition at Line 5 is true".
That will guide our further attempt at flattening.
The OR in the sentence above can be performed using the or
operator which will give us the following flattened code.
Flattened Leap Year
That is short but not exactly readable. Here is another tips: do not forget to apply previous lessons when learning new lessons. We can make code more readable by assigning expressions and using it later. Since we have duplicated expressions, we can compute once and use multiple times.
Flattened Leap Year
Numerical Properties
We can often get a better code by looking at the properties of numbers.
In this case, we need to look at some properties of divisibility.
We can see that if a year is divisible by 400, it must be divisible by 100 because 400 itself is divisible by 100.
This can also be inferred by the absence of the condition div400 and (not div100)
in our current flattened version.
Furthermore, by the same reasoning, a year that is divisible by 100 must be divisible by 4.
How can this help us?
Note that (div4 and div100 and div400)
can be simplified into div400
.
The condition in the computation is now simpler due to this simplification.
That makes the code much more readable although it may require more thinking to see that it is indeed equivalent to the previous version.
Flattened Leap Year
Summary
Summary of Composition
Summary of Flattening
The first flattening is using conjunction (i.e., and
) operator.
There is no condition on the blocks.
They can be all different.
The second flattening is using disjunction (i.e., or
) operator.
The condition for this to work is that some blocks are identical.