1.3. Control Flow Statements
In the previous chapter, we saw that a program is essentially a collection of
instructions to a computer. We saw two specific instructions: assignment
statements, which assign a value to a variable, and the print
function, which prints a value on the screen. For example:
n = 7
print("n is", n)
n = 10
print("n is now", n)
n is 7
n is now 10
The “instructions” in a program are called statements.
The above program has four statements: two assignment statements and two
function call statements (remember that print
is something called a
“function”; we will explain this construct in more detail in the next
chapter).
You might notice that these calls to print
look different than the calls
you have seen thus far. Here, we pass two comma-separated values of different
types to the function. In general,
you can pass multiple values with different types to print
,
and the function will print each of them separated by spaces.
Note
Notice that we used a different format for showing code above. Instead of
showing the execution of the program line by line in the interpreter,
we showed all of the code followed by all of its output.
In fact, if you saved the above code in a file named statements.py
and ran python statements.py
from the terminal, you would see the output shown above.
Of course, you can still run the above code in the interpreter. We are
showing you the code and the output in a different way because,
as we move on to more complex pieces of code, this format will be more
readable than the interpreter format (which can get a bit cluttered
with the >>>
prompt for every statement).
However, we will still be using
the interpreter format for small pieces of code.
These statements are run in sequence (i.e., one after the other). Sometimes, though, we want the program to run a statement based on the outcome of a previous statement, or we may want to repeat a block of statements multiple times. To support this, most programming languages include control flow statements. In this chapter, we will focus on three types of control flow statements:
Conditional statements, also known as “if statements” or “if-then-else statements”, allow us to run a piece of code only if a given condition (expressed as a boolean expression) is true, and optionally allow us to run an alternate piece of code if the condition is false. For example, given a variable
x
containing an integer, we may want to perform a different action whenx
is positive than whenx
is zero or negative (we could express this condition using the boolean expressionx > 0
).Sequence-based loops, also known as “for loops”, allow us to repeat a piece of code based on a sequence of values. For example, given a list of numbers, we may want to determine, for each number, whether the number is prime. So, we want to “repeat” the code for testing primality for each number in the list.
Condition-based loops, also known as “while loops”, allow us to repeat a piece of code based on whether a boolean expression is true. For example, when performing a simulation, we may want to continue running the simulation until some stopping condition is met.
In the next chapter, we will see additional statements that relate to functions and which also allow us to alter the control flow of our program.
1.3.1. Conditional Statements
In the previous chapter, we learned that a boolean expression is an expression
that evaluates to either True
or False
:
>>> n = 5
>>> n > 0
True
A conditional statement allows the program to perform different actions based on the value of a boolean expression. For example:
if n > 0:
print(n, "is a positive number")
In the above code, the print
statement will be run only if n
is greater than zero.
The conditional statement starts with a special keyword if
, which
is part of the syntax of the Python language. The keyword if
is
followed by a boolean expression and a colon. The next line contains
the first line of the the code to run if the boolean expression evaluates to
True
. The spacing before print
is important, and we’ll return to
this detail soon.
Let’s try out this code in the interpreter. Make sure you type four spaces before
the call to print
:
>>> n = 5
>>> if n > 0:
... print(n, "is a positive number")
...
5 is a positive number
The if
statement spans multiple lines and the interpreter won’t
run the statement until you’ve finished writing it. After you’ve written
the if
line, the interpreter will switch the prompt to ...
to
indicate that it is still expecting input as part of a multi-line statement.
After you write the print
line, the interpreter will still show the
...
prompt because it isn’t sure whether you’re going to provide more lines
in the if
statement. Pressing Enter on a blank line tells the interpreter that the code is ready to be run.
Note
Remember that the ...
prompt is an artifact of the Python interpreter.
If you were to write this code in a file, you would not include
the ...
characters. In fact, try saving this code in an if.py
file:
n = 5
if n > 0:
print(n, "is a positive number")
And running python if.py
from the terminal. The output should be:
5 is a positive number
Now, try it with a negative value of n
:
>>> n = -5
>>> if n > 0:
... print(n, "is a positive number")
...
Notice that, after running the if
statement, the interpreter didn’t produce
any output. The value of n
is not greater than zero and so, the
print
statement was not executed.
In the above example, the if
statement contained a single statement to
execute conditionally. However, conditional statements can contain multiple statements to
execute as well. For example:
n = 5
if n > 0:
print(n, "is a positive number")
n = -n
print("And now the number is negative:", n)
5 is a positive number
And now the number is negative: -5
In this case, if the condition is true, then Python will run all of the
statements under the if
. More specifically, the block of statements
under the if
, that is, all the statements at the same level of indentation
(i.e., with the same number of spaces before them), will be run. For example,
consider this other version of the code:
n = 5
if n > 0:
print(n, "is a positive number")
n = -n
print("And now the number is:", n)
5 is a positive number
And now the number is: -5
And again with a negative number:
n = -5
if n > 0:
print(n, "is a positive number")
n = -n
print("And now the number is:", n)
And now the number is: -5
Notice that the first print
and the n = -n
statement are run
only if the condition is true, but the second print
is always
run. The second print
is not part of the if
statement but occurs after the if
statement.
Python’s syntax uses indentation to
delimit blocks of code, typically with four spaces corresponding to one
level of indentation. As with other syntactical elements, Python is very
picky about this detail and incorrect indentation will produce
errors. For example, we’ll see an error if we do not indent the code in an
if statement
:
if (n % 2) == 1:
print(n, "is odd")
File "<stdin>", line 2
print(n, "is odd")
^^^^^
IndentationError: expected an indented block after 'if' statement on line 1
Or if we use inconsistent indentation inside the same block of code:
if (n % 2) == 1:
print(n, "is odd")
print("This concludes the odd branch.")
File "<stdin>", line 3
print("This concludes the odd branch.")
IndentationError: unexpected indent
For this first if
example, we walked you through all the syntactical
elements of the statement but, as you get more comfortable with Python,
it is easier to learn about new elements of the language by getting
a more formal description of their syntax. In the case of the if
statement, a formal description would look like this:
if <boolean expression>: <statements>
We will use this convention often when describing new statements and other
aspects of Python. Words in bold represent keywords that are part of
the language itself, whereas anything delimited with <
and >
means
“substitute this for …” (i.e., you do not write the <
and >
characters themselves). Additionally, remember that indentation matters, so
the statement or statements that constitute <statements>
all have the same level of indentation.
So, now we can easily introduce a variant of the if
statement in which we
specify an alternate block of statements to run if the condition is false:
if <boolean expression>: <statements> else: <statements>
For example:
n = 6
if (n % 2) == 1:
print(n, "is odd")
else:
print(n, "is even")
6 is even
When written in this format, the block under the if
is usually called
the if branch, and the block under the else
is called the
else branch.
In this example, each block has a single statement but, as we saw earlier, each block can have multiple statements:
n = 5
if (n % 2) == 1:
print(n, "is odd")
print("This concludes the odd branch.")
else:
print(n, "is even")
print("This concludes the even branch.")
5 is odd
This concludes the odd branch.
An if
statement can also have multiple if branches (but only one else
,
which is run only if none of the conditions are met). After the first if
branch, subsequent branches start with the keyword elif
(which stands for
“else if”). For example:
if <boolean expression>: <statements> elif <boolean expression>: <statements> ... else: <statements>
For example:
n = 17
if n < 0:
print(n, "is negative")
elif n > 0:
print(n, "is positive")
else:
print(n, "is zero")
17 is positive
When we have multiple branches, as soon as Python finds a true boolean
condition, it will evaluate that branch and ignore all others. Python first
evaluates the if
branch’s condition, and moves to subsequent
elif
branches only if no prior condition is true. If no branch is true, Python will run the code in the else
branch.
An important note: for any if
statement, at most one branch will be run, even if the conditions in multiple branches are true. For example:
n = 17
if n < 0:
print(n, "is negative")
elif n > 0:
print(n, "is positive")
elif n % 2 == 1:
print(n, "is odd")
elif n % 2 == 0:
print(n, "is even")
17 is positive
In the above example, there are two conditions that are true when
n
is 17: n > 0
and n % 2 == 1
. Python, however, will only
run the code for the first branch with a true condition (in this
case, n > 0
). In general, it is good practice to make the
conditions in an if
statement mutually exclusive (i.e., at most
one of them can be true).
You should also take care to distinguish between an if
statement
with multiple branches and multiple if
statements. For example,
the code below is not equivalent to the code above:
n = 17
if n < 0:
print(n, "is negative")
if n > 0:
print(n, "is positive")
if n % 2 == 1:
print(n, "is odd")
if n % 2 == 0:
print(n, "is even")
17 is positive
17 is odd
The code above has four separate if
statements (often referred
to as parallel if statements), while the previous example had a
single if
statement with four branches. Since we have four
separate statements, each conditional statement is evaluated and
Python will run the block of code for any true conditional (in this
case, the second and third if
statements).
A Common Pitfall
Using parallel if
statements when you need a single if
statement can lead to bugs that can be very hard to find. For
example,
n = 7
if n > 0:
print(n, "is positive")
n = -n
elif n < 0:
print(n, "is negative")
7 is positive
will yield a very different result than:
n = 7
if n > 0:
print(n, "is positive")
n = -n
if n < 0:
print(n, "is negative")
7 is positive
-7 is negative
Why? In the first example, the first branch of the conditional is
true and so the second branch is never tested. In the second
example, the second conditional is a separate statement. Its
test will be evaluated and since, the value of n
changes
during the execution of the first conditional the result of that
test will be True
and its statement will be executed.
Handling multiple conditions
If you have a bug in code that has a conditional statement with multiple conditions, a good first step is to verify that the conditions (i.e. the boolean expressions) are mutually exclusive. If not, ask whether your application requires overlapping conditions. If not, rewrite the conditions to make them mutually exclusive. If so, check that the order you have chosen for the conditions makes sense for the application. Also, make sure that you test each of the possible conditions. Finally, verify that you have not used parallel if statements when a single if statement with multiple conditions is required.
In general, it is a good practice to try out your code with values that will trigger the different conditions in a conditional statement.
1.3.1.1. Practice Problems
Problem 1
Assume you have an integer variable named year
that holds a a year
represented as a positive integer. Write a block of code that prints
the year and either "is leap year"
or "is not leap year"
.
To gain practice with conditionals, we recommend using conditional
statements instead of logical operators (that is, and
, or
,
not
, etc) in your solution.
Before you try to solve this problem, try to come up with some sample values for year and the expected results.
Problem 2
An instructor would like you to write block of code to determine a student’s grade based the following rules: the student’s grade is a(n):
A if the student’s midterm, final, and hw are all at least 90
B if the student’s midterm, final, and hw are all at least 80
C if the student’s midterm, final, and hw are all at least 70
B if the student’s hw is at least 90, their midterm and final are both at least 50 and their final is at least 20 points higher than their midterm. Otherwise the student should receive an F.
Your code should set a variable named grade
to the letter grade
the student earned based on the following three floating point
variables:
hw
, which holds a student’s homework average,midterm
, which holds a student’s midterm exam score, andfinal
, which holds a student’s final exam score.
What would make good sample values for hw
, midterm
, and final
?
1.3.2. for
Loops
Loops provide a mechanism for repeating work in a program.
Loops are often used when we need to process a set of values and perform
the same action for each.
For example, given a list of prices, we may want to compute
the tax on each price as well as keep a running total
of all the taxes paid. To do this task, we will use a for
loop
(in the next section, we will see a different type of loop,
the while
loop, that operates differently).
for
loops have the following syntax:
for <variable> in <sequence>: <statements>
For example:
for p in [10, 25, 5, 70, 10]:
print("The price is", p)
The price is 10
The price is 25
The price is 5
The price is 70
The price is 10
Notice that the loop repeated the print
statement five times, once for
each value in a sequence of values. In each repetition, or iteration, of the
loop, the variable p
is set to the value of a specific element in the
provided sequence of values, starting with the first and advancing
through the provided sequence. Once all of the elements have been
processed, the loop ends.
The sequence in the above for
loop ([10, 25, 5, 70, 10]
) is
called a list. We will take a deeper look at lists and other types of sequences in
Chapter Lists, Tuples, and Strings, but for now, will
only use them in for
loops. Lists are values and can be assigned to variables so
instead of including the list of values directly in the for
loop,
we could first store it in a variable:
prices = [10, 25, 5, 70, 10]
for p in prices:
print("The price is", p)
The price is 10
The price is 25
The price is 5
The price is 70
The price is 10
While in this case, we are specifying the value of prices
directly
in the program, you could imagine these prices being read from a
file, or being scanned at the checkout counter of a store.
Like if
statements, we can include multiple statements in the loop:
prices = [10, 25, 5, 70, 10]
for p in prices:
tax = 0.10 * p
total = p + tax
print("The price (with tax) is", total)
The price (with tax) is 11.0
The price (with tax) is 27.5
The price (with tax) is 5.5
The price (with tax) is 77.0
The price (with tax) is 11.0
The block of statements contained in a loop is usually called the body of the loop.
Our loop example uses a list with five values as the sequence and the
loop iterate five times and prints one line of output for each value.
If we used a list with ten values for the sequence, the loop would
iterate ten times, once per value. If the list has zero values, which
known as the empty list and is written as []
, then the loop will
iterate zero times.
prices = []
print("Before the loop")
for p in prices:
tax = 0.10 * p
total = p + tax
print("The price (with tax) is", total)
print("After the loop")
Before the loop
After the loop
Notice that the calls to print
before and after the loop, which
are not controlled by the loop, are executed and generate output. The
call to print
in the body of the loop, in contrast, is never
executed and thus generates no output, because the loop iterates zero
times.
Testing loops
A good rule of thumb for testing loops: try the loop with a list that has zero values, a list that has exactly one value, and a list that has many values.
1.3.2.1. Practice Problems
Problem 3
Given a list of integers named lst
, compute a variable total
that contains the sum of the absolute values of the elements (that is,
the values) in the list. (e.g. the list [-1, 2, 1]
would set
total
to 4
). Use the built-in function abs
to compute the
absolute value of a number. Here is are some example uses of abs
:
>>> x0 = 5
>>> abs(x0)
5
>>> x1 = -5
>>> abs(x1)
5
Before you write a solution, make a list of a few sample test cases and the corresponding expected results.
Problem 4
Given a list of booleans named lst
, write code to set a variable
contains_false
to True
if the list contains the value
False
and to False
otherwise.
Before you write a solution, make a list of a few sample test cases and the corresponding expected results.
1.3.2.2. Nested statements
Suppose we wanted to compute the total tax on all the prices. We could accomplish this task with the following loop:
prices = [10, 25, 5, 70, 10]
total_tax = 0
for p in prices:
tax = 0.10 * p
total_tax = total_tax + tax
print("The total tax is", total_tax)
The total tax is 12.0
Notice that we used an additional variable, total_tax
to add up the values
of the tax. This kind of variable is usually referred to as an accumulator
variable because it is used to add up (or accumulate) a set of values.
Now, suppose that prices with a value less than 15 are not taxed. This means
that, when computing the total tax, we should only add up the taxes of the
prices that meet the following condition: p >= 15
.
So far, the body of an if
statement or a for
loop has been a sequence
of assignments or print
statements (the only two other statements we know).
It is certainly possible, however, for the body of a for
loop to include
if
statements or even other for
loops.
For example:
prices = [10, 25, 5, 70, 10]
total_tax = 0
for p in prices:
if p >= 15:
tax = 0.10 * p
total_tax = total_tax + tax
else:
print("Skipping price", p)
print("The total tax is", total_tax)
Skipping price 10
Skipping price 5
Skipping price 10
The total tax is 9.5
Notice the two levels of indentation: one level for the body of the loop, and
another for the if
and else
branches. To be clear, we could have
other statements at the same level of indentation as the if
statement:
prices = [10, 25, 5, 70, 10]
total_tax = 0
for p in prices:
print("Processing price", p)
if p >= 15:
print("This price is taxable.")
tax = 0.10 * p
total_tax = total_tax + tax
else:
print("This price is not taxable.")
print("---")
print("The total tax is", total_tax)
Processing price 10
This price is not taxable.
---
Processing price 25
This price is taxable.
---
Processing price 5
This price is not taxable.
---
Processing price 70
This price is taxable.
---
Processing price 10
This price is not taxable.
---
The total tax is 9.5
Suppose there were three tax rates (5%, 10%, and 15%) and we wanted to find the total value of the tax under each tax rate for each of the prices. If we only had one price to worry about, we could use this single loop to compute the different taxes:
p = 10
tax_rates = [0.05, 0.10, 0.15]
print("Price", p)
for tr in tax_rates:
tax = tr * p
print("Taxed at", tr, "=", tax)
Price 10
Taxed at 0.05 = 0.5
Taxed at 0.1 = 1.0
Taxed at 0.15 = 1.5
If we had multiple prices to worry about, however, we would use a nested
for
loop. We can combine both of the single loop examples by nesting the
loop over the prices inside of the loop over tax rates to find the total tax
for different tax rates.
prices = [10, 25, 5, 70, 10]
tax_rates = [0.05, 0.10, 0.15]
for tr in tax_rates:
total_tax = 0
for p in prices:
if p >= 15:
tax = tr * p
total_tax = total_tax + tax
print("Taxed at", tr, "the total tax is", total_tax)
Taxed at 0.05 the total tax is 4.75
Taxed at 0.1 the total tax is 9.5
Taxed at 0.15 the total tax is 14.25
Notice that we replaced the fixed tax rate (0.10
) from the
original loop over the prices with the outer loop variable (tr
).
Let’s walk through this computation. For each iteration of the outer
loop:
the variable
total_tax
is set to zero, thenthe inner loop iterates through all the prices, checks whether each price (
p
) is taxable, and if so, updates the total tax using the current value oftr
, and finallythe print statement outputs the current tax rate (that is, the value of
tr
) and the computed total tax.
When you use nested loops, it is very important to think carefully
about where to initialize variables. As a thought experiment,
consider what would happen if we initialized total_tax
to zero
before (rather than in) the body of the outer loop.
prices = [10, 25, 5, 70, 10]
tax_rates = [0.05, 0.10, 0.15]
total_tax = 0
for tr in tax_rates:
for p in prices:
if p >= 15:
tax = tr * p
total_tax = total_tax + tax
print("Taxed at", tr, "the total tax is", total_tax)
Taxed at 0.05 the total tax is 4.75
Taxed at 0.1 the total tax is 14.25
Taxed at 0.15 the total tax is 28.5
Notice that rather than restarting at zero for each new tax rate, the total tax from the previous iteration of the outer loop carries over. As a result, the answers are wrong.
Do keep in mind that the right answer is context dependent. If
instead of representing different possible tax rates for a given
taxing entity (such as, a city), the rates instead represented the tax
rates for different tax domains, say the city, county, and state. If
your goal was to compute the total tax paid for all the prices across
all the taxing domains, then it would make sense to initialize
total
before the loop nest. In this use case, you would likely
move the print
statement out of the loops altogether as well.
Nested loops and initialization
If you have a bug in code that uses nested loops, verify that you are initializing all the variables used in the loop in the right place for your specific application.
1.3.2.3. Practice Problems
Problem 5
Rewrite your solution to Problem 3 to use a conditional rather than a call to abs
.
Problem 6
1.3.2.4. Iterating over ranges of integer
A common way to use for
loops is to do something with all the
integers in a given range. Let’s say we wanted to print out
which numbers between 1 and 10 are odd and even. We could do
it with this simple loop, which also features a nested if
statement:
nums = [1,2,3,4,5,6,7,8,9,10]
for n in nums:
if (n % 2) == 1:
print(n, "is odd")
else:
print(n, "is even")
1 is odd
2 is even
3 is odd
4 is even
5 is odd
6 is even
7 is odd
8 is even
9 is odd
10 is even
What if we wanted to do this computation with the numbers between 1 and 100?
While we could manually write out all those numbers, there is a better
way of doing this task: we can use Python’s built-in range
function:
for n in range(1, 11):
if (n % 2) == 1:
print(n, "is odd")
else:
print(n, "is even")
1 is odd
2 is even
3 is odd
4 is even
5 is odd
6 is even
7 is odd
8 is even
9 is odd
10 is even
We specify two numbers for range
: a lower bound and an upper
bound. The lower bound is inclusive, but the upper bound is not; i.e., range
will allow
you to iterate through every integer starting at the lower bound and
up to but not including the upper bound. To change this loop to do
our computation on the numbers between 1 and 100, we would simply
change the upper bound in the call to range
from 11 to 101 (i.e.,
range(1, 101)
).
A Common Pitfall
You should think very carefully about the appropriate bounds when
using range
. In particular, make sure you don’t make an
“off-by-one” error,
where you set a bound to be too low or too high by one. This type of
mistake is very common and can be difficult to track down.
1.3.2.5. Example: Primality testing
Let’s move on to a slightly more involved example: testing whether a number is prime. While there are very elaborate methods for testing primality, a naive way of testing whether a number \(n\) is prime is to divide that number by every integer between 2 and \(n-1\). If any such integer evenly divides \(n\), then \(n\) is not prime.
Assume that we have a variable n
containing the number we are testing.
We could use a for
loop that uses range
to iterate over every integer
between 2 and \(n-1\). Inside the loop, we would check whether
each of integer evenly divides n
using the modulus
operator (if the remainder of dividing n
by another integer is zero,
then that number can’t be prime).
Let’s take a first stab at this problem by setting up the loop, but not solving the problem entirely:
n = 81
print("Testing number", n)
for i in range(2, n):
if n % i == 0:
print(i, "divides", n)
print("Testing done")
Testing number 81
3 divides 81
9 divides 81
27 divides 81
Testing done
This code simply prints out the divisors of n
. Now let’s try
it with a prime number:
n = 83
print("Testing number", n)
for i in range(2, n):
if n % i == 0:
print(i, "divides", n)
print("Testing done")
Testing number 83
Testing done
Since 83 is a prime number and doesn’t have any divisors (other than 1 and itself),
the program doesn’t print any divisors. So, it looks like our code is on the right
track, even if it doesn’t give us a clean “yes” or “no” answer to whether n
is prime or not.
Solving a simple version of a complex problem is common practice in programming. Avoid the temptation to solve an entire problem at once, instead try solving simpler versions of the problem first to make sure you are on the right track. For example, the code above reassures us that we’re checking divisibility correctly.
So, how do we modify this code to check whether a number is prime?
Well, if n has even a single divisor, it will not be prime. So, all we
need to do is keep track of whether we have encountered a divisor or
not. While we could do this with an integer (by keeping track of how
many divisors we have encountered, and then checking whether the
number is zero or not), we can get away with using only a boolean
variable, since all we care about is whether we have encountered a
divisor or not. We initialize this variable to False
, since we haven’t
encountered any divisors yet at the start of the program.
encountered_divisor = False
n = 83
for i in range(2, n):
if n % i == 0:
encountered_divisor = True
if encountered_divisor:
print(n, "is NOT prime")
else:
print(n, "is prime")
83 is prime
Notice that the value of encountered_divisor
will not change once
it is set to True
. What would happen if we added an else
clause that set the value of encounter_divisor
to False
when
n
is not evenly divisible by i
? We’d have a bug: our code
would declare all numbers to be prime.
Finally, as an additional example of nesting one loop inside another,
suppose we want to print whether each integer in a given range is prime or not.
We could simply nest the primality testing
code we wrote above inside another for
loop that
uses range
to iterate over a sequence of integers, each of which
we will test for primality:
for n in range(2,32):
encountered_divisor = False
for i in range(2, n):
if n % i == 0:
encountered_divisor = True
if encountered_divisor:
print(n, "is NOT prime")
else:
print(n, "is prime")
2 is prime
3 is prime
4 is NOT prime
5 is prime
6 is NOT prime
7 is prime
8 is NOT prime
9 is NOT prime
10 is NOT prime
11 is prime
12 is NOT prime
13 is prime
14 is NOT prime
15 is NOT prime
16 is NOT prime
17 is prime
18 is NOT prime
19 is prime
20 is NOT prime
21 is NOT prime
22 is NOT prime
23 is prime
24 is NOT prime
25 is NOT prime
26 is NOT prime
27 is NOT prime
28 is NOT prime
29 is prime
30 is NOT prime
31 is prime
1.3.2.6. The break
statement
The primality testing algorithm we just saw is a correct algorithm:
for all values of n
(where \(n \geqslant 1\)), the algorithm
will correctly determine whether n
is prime or not. However,
it is nonetheless a terrible algorithm.
Why? Suppose you wanted to test whether \(2^{61}\) is prime. It is
clearly not prime (it is divisible by 2), but the algorithm we
presented would nonetheless waste a lot of time iterating over all
numbers from 2 to \(2^{61}\), even though we would know n
isn’t prime the moment we divided it by 2. To make the cost more
concrete, if we can perform 15,000 iterations of the loop in 1
microsecond, then determining whether \(2^{61}\) is prime using
the code above would take nearly five years.
We need to tweak our algorithm so that we can “bail out” of the
for
loop as soon as we’ve found our answer. We can use a
break
statement, which immediately exits the loop, to do this:
encountered_divisor = False
n = 83
for i in range(2, n):
if n % i == 0:
encountered_divisor = True
break
if encountered_divisor:
print(n, "is NOT prime")
else:
print(n, "is prime")
83 is prime
Notice how all we had to do was add a single break
statement in
the case when we find a divisor. Making this simple change reduces
the time to determine that \(2^{61}\) is not prime to under a
microsecond.
Even so, this algorithm is still not particularly great. For example, \(2^{61}-1\) happens to be prime and, if we used the above algorithm to test this number, we would still end up performing \(2^{61}-1\) divisions (about two quintillions, or \(2\cdot 10^{18}\)).
So, adding the break
statement only allowed us to optimize certain cases
(which might be fine if we expect to run our algorithm only with small integers).
Don’t forget that, as we discussed in Chapter Computational Thinking, computational thinking includes thinking about the complexity of computational solutions. This small example highlights that you shouldn’t settle for an algorithm simply because it produces the correct answer. Instead, you should also ask yourself whether the algorithm is sufficiently efficient for your purposes.
Sometimes, this process is as simple as asking yourself how your algorithm will perform in a couple representative cases. For example, the performance of this algorithm with a composite (not prime) number is probably fine, but the performance with very large prime numbers will be very bad. Other times, more elaborate analysis is required. We will be revisiting the notion of complexity several more times throughout this book.
Before we move on, let’s change our nested loop example to use
break
and see what happens:
for n in range(2,32):
encountered_divisor = False
for i in range(2, n):
if n % i == 0:
encountered_divisor = True
print("Breaking:", i, "evenly divides", n)
break
if encountered_divisor:
print(n, "is NOT prime")
else:
print(n, "is prime")
2 is prime
3 is prime
Breaking: 2 evenly divides 4
4 is NOT prime
5 is prime
Breaking: 2 evenly divides 6
6 is NOT prime
7 is prime
Breaking: 2 evenly divides 8
8 is NOT prime
Breaking: 3 evenly divides 9
9 is NOT prime
Breaking: 2 evenly divides 10
10 is NOT prime
11 is prime
Breaking: 2 evenly divides 12
12 is NOT prime
13 is prime
Breaking: 2 evenly divides 14
14 is NOT prime
Breaking: 3 evenly divides 15
15 is NOT prime
Breaking: 2 evenly divides 16
16 is NOT prime
17 is prime
Breaking: 2 evenly divides 18
18 is NOT prime
19 is prime
Breaking: 2 evenly divides 20
20 is NOT prime
Breaking: 3 evenly divides 21
21 is NOT prime
Breaking: 2 evenly divides 22
22 is NOT prime
23 is prime
Breaking: 2 evenly divides 24
24 is NOT prime
Breaking: 5 evenly divides 25
25 is NOT prime
Breaking: 2 evenly divides 26
26 is NOT prime
Breaking: 3 evenly divides 27
27 is NOT prime
Breaking: 2 evenly divides 28
28 is NOT prime
29 is prime
Breaking: 2 evenly divides 30
30 is NOT prime
31 is prime
Upon encountering a break
, Python exits the innermost enclosing
loop. We added a print
statement to highlight this behavior. As
you can see from the output, upon finding a value of i
that evenly
divides the current value of n
, Python prints a message with the
values of i
and n
and moves on to the conditional that follows
the inner loop.
Keep in mind that break
statements are always used in a loop
guarded by a conditional statement. A loop with an unguarded
break
statement will execute at most once. This loop, for
example,
prices = [10, 25, 5, 70, 10]
for p in prices:
print("The value of p is:", p)
break
The value of p is: 10
executes outputs exactly one line. If list of prices happens to be empty, then the loop would not generate any output, which explains why we wrote that a loop with an unguarded break would execute “at most” once.
1.3.2.7. Practice Problems
Problem 6
Rewrite your solution to Problem 4 to stop the loop looking at values
once you find the first False
in the list.
1.3.2.8. continue
statement
Python will immediately exit a loop when it encounters a break
statement. Sometimes, you just want to stop the current iteration and
move on to the next one. To illustrate this concept, we will rewrite
an earlier example using continue
:
prices = [10, 25, 5, 70, 10]
total_tax = 0
for p in prices:
if p < 15:
print("Skipping price", p)
continue
tax = 0.10 * p
total_tax = total_tax + tax
print("The total tax is", total_tax)
Skipping price 10
Skipping price 5
Skipping price 10
The total tax is 9.5
In this version, we flipped the test to be p < 15
(rather than p
>= 15
), added a use of continue
after the call to print
in the
conditional, and reduced the indentation for the rest of the loop
body.
While continue
should be used sparingly as it can lead to
confusing code, it can help improve readability when you have a
complex deeply-nested piece of code that needs to be executed in some
situations and not others. Writing this code:
for <variable> in <sequence>:
if not <simple test>:
<complex, deeply nested code>
this way:
for <variable> in <sequence>:
if <simple test>:
continue
<complex, deeply nested code>
may yield code that is easier to understand. And so, while our
example above is correct, we would not normally choose to write code
that simple using continue
.
As with break
, it does not make sense to use continue
without
a corresponding conditional to guard its execution as the code
following an unguarded continue
would never be executed.
1.3.3. while
loops
while
loops are a more general type of loop that, instead of repeating an action for each
element in a sequence, will repeat an action while a condition is true.
The condition is expressed using a boolean expression, which allows us
to write much more complex loops than for
loops.
The syntax for while
loops is the following:
while <boolean expression>: <statements>
Python starts executing a while
loop by evaluating the boolean expression
and, if it is true, running the block of statements (otherwise, the while
loop is skipped entirely).
After the statements are run, the boolean expression is evaluated again, and
if it remains true, the statements are run again. This process is repeated
until the boolean expression becomes false.
For example, this while
loop will add up all of the integers between 1 and N
:
N = 10
i = 1
sum = 0
while i <= N:
sum = sum + i
i = i + 1
print(sum)
55
We use a sum
variable to store the sum of all the integers, and initialize
it to 0. Then, we use a variable i
to represent the integer we are adding
to the sum. We initialize it to one and, in each iteration of the loop, we add
i
to the sum, and increment i
by one. As long as i
is less than N
,
the loop will keep adding i
to sum
.
Unlike a for
loop, a while
loop does not require a
sequence of values. Instead, we must keep track of and update
the integer we add in each iteration of the loop
(stored in variable i
).
This difference is often the source of a common programming
error: forgetting to include code to increment i
(that is, the
statement: i = i + 1
). Without this statement, the loop
becomes an infinite loop: the``while`` condition (i <= N
)
will never become false because the value of
i
will always be one (and thus will
never become greater than N
). When this happens, your program (or
the Python interpreter) will appear stuck. In most computer
environments, you can force your program to exit by pressing the
“Control” key and the “C” key (known as “Control-C”) at the same time.
You may have to type “Control-C” a few times to get Python’s attention.
In fact, precisely because of this issue, the above piece of code should be
implemented using a for
loop, which is less error prone:
N = 10
sum = 0
for i in range(N+1):
sum = sum + i
print(sum)
55
Notice that we cannot possibly fall into an infinite loop with this
implementation, because the for
loop
will only iterate over the specified range of values.
So when should we use a for
loop instead of a while
loop?
As a rule of thumb, any time you need to iterate over a sequence of
values, using a for
loop is typically the best option.
Although a while
loop can get the job done, it
can be more error-prone.
There are, however, certain algorithms where the loop cannot naturally be stated as iterating over a sequence of values, so we need the more general mechanism provided by a boolean expression. We will see one such example in the next section.
1.3.3.1. Practice Problems
Problem 7
Using only arithmetic and relational operators, conditional
statements, and a while loop: determine the nearest power of 2 that is
greater than or equal to a value N
. You may assume that N >
0
.
What would make good test cases for this computation?
Problem 8
Using only arithmetic and relational operators, conditional
statements, and a while loop: determine the nearest power of 2 that is
less than or equal to a value N
. You may assume that N > 0
.
What would make good test cases for this computation?
1.3.4. Putting it all together
In this section we are going to work through a slightly longer example that combines conditionals and loops and involves non-trivial logic.
A few years ago Gil Kalai posed a question on his blog Combinatorics and more:
You throw a dice until you get 6. What is the expected number of throws (including the throw giving 6) conditioned on the event that all throws gave even numbers.
The answer, somewhat paradoxically, is 1.5. As noted by Dan Jacob Wallace, the question can be answered using some probability theory and pre-calculus, but it can also be answered by simulating a simple game many times and averaging the number of throws per game.
In the game, we will roll one fair six-sided die over and over again and count the number of rolls. Since the problem is conditioned on all the throws being even numbers, we will stop a given game and declare it to be invalid as soon as we roll an odd number. For valid games, we will keep rolling the die until we hit a six.
Before we can translate this informal description into code, we need
to find a way to simulate rolling a die. We can use a function,
randint
from Python’s built-in random
library for this
purpose. Given a lower bound and an upper bound, the function chooses
a value uniformly at random between the lower bound and upper bound
inclusive (that is, including both end points). Since we are working
with a six-sided die, we will call random.randint(1, 6)
repeatedly
to get a randomly-chosen stream of values that range between 1
and
6
inclusive.
Note
random.randint
is a function, which we will be discussing in
more detail in the next chapter. For now, all you need to so is that,
like the print
function, this function will
perform a task for us and, in this case, it will be choosing a
number randomly between 1
and 6
. In chapter Basics of Code Organization
we will also discuss Python’s built-in libraries, like random
in more detail.
To simulate the game, we will need to roll the die over and over again
until one of two conditions is met: either (1) a six is rolled, which
ends the (valid) game or (2) an odd number is rolled, which signals
that the game is invalid and should end. while
loops are designed for
exactly this type of computation: we want to execute a piece of code
repeatedly while some conditions holds. Here’s one–quite direct–way
to translate this description into code:
# Simulate a new game
is_valid_game = True
# first roll
roll = random.randint(1, 6)
game_num_rolls = 1
while roll != 6:
if roll % 2 == 1:
# stop: hit an odd number making the game invalid
is_valid_game = False
break
# subsequent rolls
roll = random.randint(1, 6)
game_num_rolls = game_num_rolls + 1
At the end of the game, the variable game_num_rolls
will hold the
number of times the die was thrown in the game and the variable
is_valid_game
will tell us whether the game was valid.
Here is another less direct, but easier-to-read version:
# first roll
roll = random.randint(1, 6)
game_num_rolls = 1
while roll == 2 or roll == 4:
# subsequent rolls
roll = random.randint(1, 6)
game_num_rolls = game_num_rolls + 1
is_valid_game = (roll == 6)
In this version, we keep rolling the die until we hit something other
than a 2
or a 4
(i.e., a 1
, 3
, 5
, or 6
). We
mark the game as valid only if the last roll, the one that caused the
game to stop, was a 6
.
These two versions illustrate a common phenomenon: there is often more than one way to implement a task and thinking about it in a slightly different way may yield simpler code.
Let’s move on to simulating our game many times and computing the
answer. We can run multiple games or trials by wrapping a for-loop
around the code for simulating a game. Our goal is to compute the
average number of rolls per valid game. To do so, we will track the
number of valid games using the variable num_valid_games
and the
total number of rolls made in valid games using the variable
total_rolls_valid_games
.
Here’s code that for the full task:
num_trials = 1000
num_valid_games = 0
total_rolls_valid_games = 0
for _ in range(num_trials):
# run a new game
# first roll
roll = random.randint(1, 6)
game_num_rolls = 1
while roll == 2 or roll == 4:
# subsequent rolls
roll = random.randint(1, 6)
game_num_rolls = game_num_rolls + 1
if roll == 6:
# the game was valid
num_valid_games = num_valid_games + 1
total_rolls_valid_games = total_rolls_valid_games + game_num_rolls
if num_valid_games > 0:
ans = total_rolls_valid_games/num_valid_games
print("Average number of rolls in valid games:", ans)
else:
print("No valid games. Try again.")
The value of the loop variable for the outer for
loop is not needed in the
computation and so, we followed Python convention and named it _
(underscore).
Note that we dropped the name is_valid_game
and simply used the
expression roll == 6
as the test for a conditional that ensures
that num_valid_games
and total_rolls_valid_games
are updated
only for valid games.
Finally, let’s review our decisions about where to initialize the
variables used in the computation. The variables num_valid_games
and total_rolls_valid_games
are initialized before the outer loop
because they are used to accumulate values across the whole
computation. In contrast, roll
and game_num_rolls
are
re-initialized for every iteration of the outer loop, because they
need to be reset to their initial values for each new game.
1.3.5. Practice Problem Solutions
Problem 1:
Here are some sample values for year
and the expected output:
2024 is a leap year
2000 is a leap year
1900 is not a leap year
2027 is not a leap year
Here is one approach to using conditional statements and relational
operators, but not logical and
or logical or
to determine
whether a year year
with a positive integer represents a leap
year:
if year % 400 == 0:
print(f"{year} is a leap year")
elif year % 4 == 0:
if year % 100 != 0:
print(f"{year} is a leap year")
else:
print(f"{year} is not a leap year")
else:
print(f"{year} is not a leap year")
Here is an alternative way to write this code:
if year % 4 == 0:
if year % 100 != 0:
print(f"{year} is a leap year")
elif year % 400 == 0:
print(f"{year} is a leap year")
else:
print(f"{year} is not a leap year")
else:
print(f"{year} is not a leap year")
And here is a incorrect approach:
# This code is incorrect.
if year % 4 == 0:
if year % 100 != 0:
print(f"{year} is a leap year")
else:
print(f"{year} is not a leap year")
elif year % 400 == 0:
print(f"{year} is a leap year")
else:
print(f"{year} is not a leap year")
This code produces the right answer for three of the four sample years. Which years will be tagged correctly and which year will be tagged incorrectly? Why?
Problem 2
Here are some possible test cases as the expected value for grade
:
Midterm |
Final |
Homework |
Grade |
---|---|---|---|
90.0 |
90.0 |
92.0 |
A |
80.0 |
85.5 |
80.0 |
B |
70.0 |
70.0 |
73.5 |
C |
50.0 |
70.0 |
90.0 |
B |
70.0 |
50.0 |
90.0 |
F |
50.0 |
70.0 |
85.0 |
F |
Here is a block of code that sets the variable grade
to the grade
a student earned using their homework (hw
), final (final
), and
midterm (midterm
) scores:
if final >= 90 and midterm >= 90 and hw >= 90:
grade = 'A'
elif final >= 80 and midterm >= 80 and hw >= 80:
grade = 'B'
elif final >= 70 and midterm >= 70 and hw >= 70:
grade = 'C'
elif final >= 50 and midterm >= 50 and hw >= 90 and final >= (midterm+20):
grade = 'B'
else:
grade = 'F'
Problem 3
|
|
---|---|
|
0 |
|
5 |
|
5 |
|
43 |
Here is a block of code that computes the sum of the absolute values in lst
:
total = 0
for val in lst:
total = total + abs(val)
return total
Problem 4
Here are some sample test cases:
|
|
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
result = False
for val in lst:
# note the use of not rather than
# val == False.
if not val:
result = True
Notice that this solution looks at every value in the list even though
we know the value of result
won’t change once it is set to
True
. We’ll see a better way to write this code in Exercise 6.
Problem 5
Here is a version of the code that computes the sum of the absolute
values in a list using a conditional rather than a call to abs
.
total = 0
for val in lst:
if val < 0:
total = total - val
else:
total = total + val
Problem 6
This code uses break
to stop the loop upon finding the first
instance of False
in the list.
result = False
for val in lst:
if not val:
result = True
# we can stop as soon as we find a False
break
Problem 7
Here are some sample tests:
Value of |
Expected value for |
---|---|
1 |
1 |
2 |
2 |
5 |
8 |
And here is a solution:
nearest = 1
while (nearest < n):
nearest = nearest * 2
Problem 8
Here are some test cases:
Value of |
Expected value for |
---|---|
1 |
1 |
2 |
2 |
23 |
16 |
And here is a solution:
nearest = 1
while (2*nearest <= n):
nearest = nearest * 2