CMSC 14100 — Lecture 4

Like arithmetic expressions, there is an order of operations for boolean connectives:

  1. negation (not),
  2. conjunction (and),
  3. disjunction (or).

Typically, expressions in Python are evaluated completely (also called eager evaluation), but if you look at the table above you might notice that this isn't strictly necessary for the boolean connectives. For example, the expression


    1 == 2 and 2 > 1
    

is false, because 1 == 2 evaluates to False. This means that there is no need to evaluate the expression on the right, because no matter what it evaluates to, the result of the expression is already known. A similar phenomenon occurs with disjunction as well: in an expression x or y, if x evaluates to True, then we know that x or y will evaluate to True without having to evaluate y.

Python will take advantage of this property to avoid evaluating every expression in a boolean expression. If an operand in a conjunction evaluates to False or an operand in a disjunction evaluates to True, Python will immediately evaluate the expression without evaluating the rest of the operands in the expression. This behaviour is called short-circuiting. This makes it worthwhile to consider the order of the operands in a boolean expression.

Recall that a year is a leap year if the year is divisible by 4 and not divisible by 100 unless it is divisible by 400. (Not quite as straightforward as many people might believe!) We can express this in the following way.


year = 2022
((year % 4 == 0) and (year % 100 != 0)) or (year % 400 == 0)
(year % 4 == 0) and ((year % 100 != 0) or (year % 400 == 0))
    

Both of these expressions will give the same result, but they exhibit different short-circuting behaviour. For example, in the first expression, we test year % 4 == 0. If this is False, then we must still test year % 400 == 0. But it can't be the case that a number is not divisible by 4 while still being divisible by 400.

If we reorganize the expression slightly, as in the second expression, we immediately stop evaluation of the expression once we've determined our year is not divisible by 4.

Control structures

In the imperative programming paradigm, a program is a series of statements (imperatives!) that are executed in order. These are different from expressions—expressions are evaluated into a value, while statements are instructions to the computer that. Some examples of statements we've seen are assignment statements (store a value in memory) and function calls (namely print, which displays a value).

So far, our programs have simply been a series of statements which are executed one after the other. We have been able to use such statements to manage data, but we don't yet have any mechanism to make decisions about which instructions to execute based on that information. We will now introduce control statements which are used to alter the control flow of the program, allowing us to skip or repeat statements.

Conditional statements

Conditional statements allow the program to execute different actions based on a condition, expressed as a boolean expression. For example, a program may behave differently depending on whether or not a value was odd or even. In this case, the condition we would need to test is the expression


    n % 2 == 0
    

This expression evaluates to True if n is even and it evaluates to False if n is odd.

The template for a basic conditional statement is


    if <boolean expression>:
        <statements>
    else:
        <statements>
    

This statement can be read as "if the condition holds (i.e. it is true), then perform this set of actions; otherwise (i.e. or else), perform that set of actions".

Then we can write the following,


    if n % 2 == 0:
        print(n, "is even")
    else:
        print(n, "is odd")
    

Within a branch, multiple statements can be executed. In Python, blocks of statements are denoted only by indentation. This is very important! Because there is no other denotation, Python is particularly sensitive about indentation. In principle, any amount of indentation is acceptable, as long as the block is indented consistently. However, in practice, the Python style guide calls for four (4) spaces (not tabs) for an indentation level and this is almost universally adhered to.

Often times, we will want to test for a condition that has multiple outcomes. For instance, suppose in addition to testing the parity of a number, we also want to determine whether it is negative or not. We can write


    if n < 0:
        print(n, "is negative")
    else:
        if n % 2 == 0:
            print(n, "is nonnegative and even")
        else:
            print(n, "is positive and odd")
    

Note that a conditional statement is a statement, so we can use it within another conditional! While this works, and sometimes it is useful to organize cases like this, such sprawling nested conditional branches can become unwieldy. Instead, we can use elif to add cases on one level.


    if n < 0:
        print(n, "is negative")
    elif n % 2 == 0:
        print(n, "is nonnegative and even")
    else:
        print(n, "is positive and odd")
    

It's important to keep in mind that only one case of a conditional statement is ever executed. A common error is to create a separate conditional statement for each case, such as in the following.


    if n < 0:
        print(n, "is negative")
    if n % 2 == 0:
        print(n, "is nonnegative and even")
    if n % 2 == 1:
        print(n, "is positive and odd")
    

Here, it is possible for two cases to be executed if $n$ is negative.