Chapter 4. Flow Control and Logic
Flow control is a high-level way of programming a computer to make decisions. These decisions can be simple or complicated, executed once or multiple times. The syntax for the different flow control mechanisms varies, but what they all share is that they determine an execution pathway for the program. Python has relatively few forms of flow control. They are conditionals, exceptions, and loops.
As someone primarily interested in physical reality, you might wonder why you should care about flow control and logic. In some ways, this is like asking why arithmetic is important. Logic presents rules that allow you to build up and represent more complex ideas. This enables the physics modeling you want to do by giving you a means to express the choices and behavior of your model to the computer. With basic flow control syntax, your models can make simple decisions. With more advanced flow control, your models can make more sophisticated choices more easily. In other situations, flow control allows you to reuse the same code many times. This makes the software model faster to write and easier to understand, because it has fewer total lines of code. Logic and flow control are indispensible to doing any significant amount of work with computers. So, without further delay, let’s jump into conditionals, our first bit of flow control.
Conditionals
Conditionals are the simplest form of flow control. In English, they follow the
syntax “if x
is true, then do something; otherwise, do something else.” The shortest conditional is when there is only an if
statement on its own. The
format for such a statement is as follows:
if
<
condition
>
:
<
if
-
block
>
Here, the Python keyword if
is followed by an expression,
, which is
itself followed by a colon (<condition>
:
). When the Boolean representation of the condition,
bool(condition)
, is True
, the code that is in the
is executed.
If <if-block>
bool(condition)
is False
, then the code in the block is skipped.
The condition may be composed of any of the comparison
operators (or a combination of these operators) that were listed in Table 2-1.
For convenience, just the comparison operators are shown again here in Table 4-1.
Name | Usage | Returns |
---|---|---|
Unary operators |
||
Negation |
|
Logical negation— |
Bitwise invert |
|
Changes all zeros to ones and vice versa in |
Binary operators |
||
Logical and |
|
|
Logical or |
|
|
Comparison binary operators |
||
Equality |
|
|
Not equal |
|
|
Less than |
|
|
Less than or equal |
|
|
Greater than |
|
|
Greater than or equal |
|
|
Containment |
|
|
Non-containment |
|
|
Identity test |
|
|
Not identity test |
|
|
Ternary operators |
||
Ternary compare |
|
|
For example, if we wanted to test if Planck’s constant is equal to one and then change its value if it is, we could write the following:
h_bar
=
1.0
if
h_bar
==
1.0
:
(
"h-bar isn't really unity! Resetting..."
)
h_bar
=
1.05457173e-34
Here, since h_bar
is 1.0
it is reset to its actual physical value (1.05457173e-34
).
If h_bar
had been its original physical value, it would not have been reset.
A key Pythonism that is part of the if
statement is that Python is
whitespace separated. Unlike other languages, which use curly braces and semicolons, in Python the contents of the
if
block are determined by their indentation level. New statements must appear
on their own lines. To exit the if
block, the indentation level is returned back
to its original column:
h_bar
=
1
if
h_bar
==
1
:
(
"h-bar isn't really unity! Resetting..."
)
h_bar
=
1.05457173e-34
h
=
h_bar
*
2
*
3.14159
The last line here (the one that defines h
) indicates that the if
block has ended
because its indentation level matches that of the if
on the second line.
The last line will always be executed,
no matter what the conditional decides should be done for the if
block.
While we are on the subject, it is important to bring up the distinction between
the equality operator (
) and the identity operator (==
). The equality
operator tests if two values are equivalent. For example,
is
is 1 == 1.0
even though True
is an integer and 1
is a float.
On the other hand, the identity operator tests if two variable names
are references to the same underlying value in memory. For example,
1.0
is 1 is 1.0
because the types are different, and therefore they cannot
actually be references to the same value. False
is much faster than is
, but also much more
strict. In general, you want to use ==
for singletons like is
and
use the safer None
in most other situations. The following examples
show typical use cases and gotchas:==
Code | Output |
---|---|
True True False True True False True False False |
Before we move on, it is important to note that, by tradition, Python uses four spaces per level to indent all code blocks. Two spaces, eight spaces, or any other spacing is looked down upon. Tabs cause many more problems than they are worth. Most text editors have an option to automatically convert tabs to spaces, and enabling this can help prevent common errors. Some people find the whitespace syntax a little awkward to begin with, but it becomes easy and natural very quickly. The whitespace-aware aspect of Python is a codification of what is a best-practice coding style in other languages. It forces programmers to write more legible code.
if-else Statements
Every if
statement may be followed by an optional else
statement. This is the
keyword else
followed by a colon (:
) at the same indentation level as the
original if
. The
lines following this are indented just like the
<else-block>
if
block. The code in the else
block is executed when the condition is False
:
if
<
condition
>
:
<
if
-
block
>
else
:
<
else
-
block
>
For example, consider the expression sin(1/x)
. This function is computable everywhere
except a x = 0
. At this point, L’Hôpital’s rule shows that the result is also
zero. This could be expressed with an if-else
statement as follows:
if
x
==
0
:
y
=
0
else
:
y
=
sin
(
1
/
x
)
This is equivalent to negating the conditional and switching the if
and else
blocks:
if
x
!=
0
:
y
=
sin
(
1
/
x
)
else
:
y
=
0
However, it is generally considered a good practice to use positive conditionals (==
)
rather than negative ones (!=
). This is because humans tend to think
about an expression
being true rather than it being false. This is not a hard and fast rule, but it does
help eliminate easy-to-miss logic bugs.
if-elif-else Statements
Python also allows multiple optional elif
statements. The elif
keyword is an
abbreviation for “else if,” and such statements come after the if
statement and before
the else
statement.
The elif
statements have much the same form as the if
statement, and there may be as
many of them as desired. The first conditional that evaluates to True
determines the
block that is entered, and no further conditionals or blocks are executed. The syntax is as follows:
if
<
condition0
>
:
<
if
-
block
>
elif
<
condition1
>
:
<
elif
-
block1
>
elif
<
condition2
>
:
<
elif
-
block2
>
...
else
:
<
else
-
block
>
Suppose that you wanted to design a simple mid-band filter whose signal is 1 if the
frequency is between 1 and 10 Hertz and 0 otherwise. This could be done with an if-elif-else
statement:
if
omega
<
1.0
:
signal
=
0.0
elif
omega
>
10.0
:
signal
=
0.0
else
:
signal
=
1.0
A more realistic example might include ramping on either side of the band:
if
omega
<
0.9
:
signal
=
0.0
elif
omega
>
0.9
and
omega
<
1.0
:
signal
=
(
omega
-
0.9
)
/
0.1
elif
omega
>
10.0
and
omega
<
10.1
:
signal
=
(
10.1
-
omega
)
/
0.1
elif
omega
>
10.1
:
signal
=
0.0
else
:
signal
=
1.0
if-else Expression
The final syntax covered here is the ternary conditional operator.
It allows simple if-else
conditionals to be evaluated
in a single expression. This has the following syntax:
x
if
<
condition
>
else
y
If the condition evaluates to True
, then x
is returned. Otherwise, y
is returned.
This turns out to be extraordinarily handy for variable assignment. Using this kind of expression, we
can write the h_bar
conditional example in one line:
h_bar
=
1.05457173e-34
if
h_bar
==
1.0
else
h_bar
Note that when using this format you must always include the else
clause. This
fills the same role as the condition?x:y
operator that is available in other
languages. Writing out if
and else
arguably makes the Python way much more
readable, though also more verbose.
Exceptions
Python, like most modern programming languages, has a mechanism for exception handling. This is a language feature that allows the programmer to work around situations where the unexpected and catastrophic happen. Exception handling is for the truly exceptional cases: a user manually types in an impossible value, a file is deleted while it is being written, coffee spills on the laptop and fries the motherboard.
Warning
Exceptions are not meant for normal flow control and dealing with expected behavior! Use conditionals in cases where behavior is anticipated.
The syntax for handling exceptions is known as a
try-except
block. Both try
and except
are Python keywords. try-except
s
look very similar to if-else
statements, but without the condition:
try
:
<
try
-
block
>
except
:
<
except
-
block
>
The try
block will attempt to execute its code. If there are no errors, then the
program skips the except
block and proceeds normally. If any error at all happens,
then the except
block is immediately entered, no matter how far into the try
block
Python has gone. For this reason, it is generally a good idea to keep the try
block
as small as possible. Single-line try
blocks are strongly preferred.
As an example, say that a user manually inputs a value and then the program takes
the inverse of this value. Normally this computes just fine, with the exception of
when the user enters 0
:
In
[
1
]:
val
=
0.0
In
[
2
]:
1.0
/
val
ZeroDivisionError
Traceback
(
most
recent
call
last
)
<
ipython
-
input
-
2
-
3
ac1864780ca
>
in
<
module
>
()
---->
1
1.0
/
val
ZeroDivisionError
:
float
division
by
zero
This error could be handled with a try-except
, which would prevent the program from
crashing:
try
:
inv
=
1.0
/
val
except
:
(
"A bad value was submitted {0}, please try again"
.
format
(
val
))
The except
statement also allows for the precise error that is anticipated to be
caught. This allows for more specific behavior than the generic catch-all exception.
The error name is placed right after the except
keyword but before the colon.
In the preceding example, we would catch a ZeroDivisionError
by writing:
try
:
inv
=
1.0
/
val
except
ZeroDivisionError
:
(
"A zero value was submitted, please try again"
)
Multiple except
blocks may be chained together, much like elif
statements. The
first exception that matches determines the except
block that is executed.
The previous two examples could therefore be combined as follows:
try
:
inv
=
1.0
/
val
except
ZeroDivisionError
:
(
"A zero value was submitted, please try again"
)
except
:
(
"A bad value was submitted {0}, please try again"
.
format
(
val
))
Raising Exceptions
The other half of exception handling is raising them yourself.
The raise
keyword will throw an exception
or error, which may then be caught by a try-except
block elsewhere. This syntax
provides
a standard way for signaling that the program has run into an unallowed situation and
can no longer continue executing.
raise
statements may appear anywhere, but it is common to put them inside of
conditionals so that they are not executed unless they need to be. Continuing with the
inverse example, instead of letting Python raise a ZeroDivisionError
we could check for a zero value and raise it ourselves:
if
val
==
0.0
:
raise
ZeroDivisionError
inv
=
1.0
/
val
If val
happens to be zero, then the inv = 1.0 / val
line will never be run. If val
is nonzero, then the error is never raised.
All errors can be called with a custom string message. The helps locate, identify, and squash bugs. Error messages should be as detailed as necessary while remaining concise and readable. A message that states “An error occurred here” does not help anyone! A better version of the preceding code is:
if
val
==
0.0
:
raise
ZeroDivisionError
(
"taking the inverse of zero is forbidden!"
)
inv
=
1.0
/
val
Python comes with 150+ error and exception types. (This is not as many as it seems at first glance—these exceptions are sufficient to cover the more than one million lines of code in Python itself!) Table 4-2 lists some of the most common ones you will see in computational physics.
Exception | Description |
---|---|
|
Used when the |
|
Occurs when Python cannot find a variable that lives on another variable. Usually this results from a typo. |
|
Occurs when a package or module cannot be found. This is typically the result of either a typo or a dependency that hasn’t been installed. |
|
Happens when Python cannot read or write to an external file. |
|
Automatically raised when an external user kills the running Python process with Ctrl-c. |
|
Raised when a key cannot be found in a dictionary. |
|
Raised when your computer runs out of RAM. |
|
Occurs when a local or global variable name cannot be found. Usually the result of a typo. |
|
Generic exception for when something, somewhere has gone wrong. The error message itself normally has more information. |
|
Raised when the program tries to run non-Python code. This is typically the result of a typo, such as a missing colon or closing bracket. |
|
Occurs when Python has tried to divide by zero, and is not happy about it. |
It is often tempting to create custom exceptions for specific cases. You’ll find more information on how to do this in Chapter 6. However, custom exception types are rarely necessary—99% of the time there is already a built-in error that covers the exceptional situation at hand. It is generally better to use message strings to customize existing error types rather than creating brand new ones.
Loops
While computers are not superb at synthesizing new tasks, they are very good at
performing the same tasks over and over. So far in this chapter, we’ve been discussing the single execution of indented code blocks. Loops are how to execute
the same block multiple times. Python has a few looping formats that
are essential to know: while
loops, for
loops, and comprehensions.
while Loops
while
loops are related to if
statements because they continue to execute “while a condition is true.” They have nearly the same syntax, except the if
is replaced with the while
keyword. Thus, the syntax has the following format:
while
<
condition
>
:
<
while
-
block
>
The condition here is evaluated right before every loop iteration. If the condition
is or remains True
, then the block is executed. If the condition is False
, then the
while
block is skipped and the program continues. Here is a simple countdown timer:
Code | Output |
---|---|
|
t-minus 3 t-minus 2 t-minus 1 blastoff! |
If the condition evaluates to False
, then the while
block will never be entered.
For example:
Code | Output |
---|---|
|
I can |
On the other hand, if the condition always evaluates to True
, the while
block will
continue to be executed no matter what. This is known as an infinite or nonterminating loop. Normally this is not the intended behavior. A slight
modification to the countdown timer means it will never finish on its own:
Code | Output |
---|---|
|
t-minus 3
t-minus 2
t-minus 1
t-minus 0
t-minus -1
t-minus -2
t-minus -3
t-minus -4
t-minus -5
...
|
Integers counting down to negative infinity is not correct behavior in most situations.
Note
Interestingly, it is impossible to predict whether a loop (or any program) will terminate without actually running it. This is known as the halting problem and was originally shown by Alan Turing. If you do happen to accidentally start an infinite loop, you can always hit Ctrl-c to exit the Python program.
The break
statement is Python’s way of leaving a loop early. The keyword
break
simply appears on its own line, and the loop is immediately
exited. Consider the following while
loop, which computes successive elements of the
Fibonacci series and adds them to the fib
list. This loop will continue forever
unless it finds an entry that is divisible by 12, at which point it will immediately
leave the loop and not add the entry to the list:
Code | Output |
---|---|
|
|
This loop does terminate, because
and 55 + 89 == 144
.
Also note that the 144 == 12**2
if
statement is part of the while
block. This means that the
break
statement needs to be additionally indented. Additional levels of indentation
allow for
code blocks to be nested within one another. Nesting can be arbitrarily deep
as long as the correct flow control is used.
for Loops
Though while
loops are helpful for repeating statements, it is typically more
useful to iterate over a container or other “iterable,” grabbing a single element
each time through and exiting the loop when there are no more elements in the
container. In Python, for
loops fill this role. They use the for
and in
keywords and have the following syntax:
for
<
loop
-
var
>
in
<
iterable
>
:
<
for
-
block
>
The <loop-var>
is a variable name that is assigned to a new element of the iterable
on each pass through the loop. The <iterable>
is any Python object that can return elements.
All containers (lists, tuples, sets, dictionaries) and strings are iterable. The for
block
is a series of statements whose execution is repeated. This is the same as what was
seen for while
blocks. Using a for
loop, we could rewrite our countdown timer to
loop over the list of integers [3, 2, 1]
as follows:
for
t
in
[
3
,
2
,
1
]:
(
"t-minus "
+
str
(
t
))
(
"blastoff!"
)
Again, the value of t
changes on each iteration. Here, though, the
t = t - 1
line is not needed because t
is automatically reassigned to the
next value in the list. Additionally, the 0 < t
condition is not needed to stop the
list; when there are no more elements in the list, the loop ends.
The break
statement can be used with for
loops just like with while
loops. Additionally,
the continue
statement can be used with both for
and while
loops. This exits out
of the current iteration of the loop only and continues on with the next iteration.
It does not break out of the whole loop. Consider the case where we want to count down
every t
but want to skip reporting the even times:
Code | Output |
---|---|
|
t-minus 7 t-minus 5 t-minus 3 t-minus 1 blastoff! |
Note that containers choose how they are iterated over. For sequences (strings, lists, tuples), there is a natural iteration order. String iteration produces each letter in turn:
Code | Output |
---|---|
|
G o r g u s |
However, unordered data structures (sets, dictionaries) have an unpredictable iteration ordering. All elements are guaranteed to be iterated over, but when each element comes out is not predictable. The iteration order is not the order that the object was created with. The following is an example of set iteration:
Code | Output |
---|---|
|
0 True Gorgus |
Dictionaries have further ambiguity in addition to being unordered. The loop variable
could be the keys, the values, or both (the items). Python chooses to
return the keys when looping over a dictionary. It is assumed that
the values can be looked up normally. It is very common to use key
or k
as the loop variable name. For example:
Code | Output |
---|---|
|
birthday |
Dictionaries may also be explicitly looped through their keys, values, or items using
the keys()
, values()
, or items()
methods:
Code | Output |
---|---|
|
Keys: birthday last |
When iterating over items, the elements come back as key/value tuples. These can
be unpacked into their own loop variables (called key
and
value
here for consistency, though this is not mandatory). Alternatively, the items could remain packed, in which case the
loop variable would still be a tuple:
Code | Output |
---|---|
|
|
It is a very strong idiom in Python that the loop variable name is a singular noun and the iterable is the corresponding plural noun. This makes the loop more natural to read. This pattern expressed in code is shown here:
for
single
in
plural
:
...
For example, looping through the set of quark names would be done as follows:
quarks
=
{
'up'
,
'down'
,
'top'
,
'bottom'
,
'charm'
,
'strange'
}
for
quark
in
quarks
:
(
quark
)
Comprehensions
for
and while
loops are fantastic, but they always take up at least two lines: one for
the loop itself and another for the block.
And often when you’re looping
through a container the result of each loop iteration needs to be placed in a
new corresponding list, set, dictionary, etc. This takes at least three lines.
For example, converting
the quarks set to a list of uppercase strings requires first setting up an
empty list:
Code | Output |
---|---|
|
|
However, it seems as though this whole loop could be done in one line.
This is because there is only one
meaningful expression where work is performed:
namely upper_quarks.append(quark.upper())
. Enter comprehensions.
Comprehensions are a syntax for spelling out simple for
loops
in a single expression. List, set, and dictionary comprehensions exist, depending
on the type of container that the expression should return. Since they are simple,
the main limitation is that the for
block may only be a single expression itself.
The syntax for these is as follows:
# List comprehension
[
<
expr
>
for
<
loop
-
var
>
in
<
iterable
>
]
# Set comprehension
{
<
expr
>
for
<
loop
-
var
>
in
<
iterable
>
}
# Dictionary comprehension
{
<
key
-
expr
>
:
<
value
-
expr
>
for
<
loop
-
var
>
in
<
iterable
>
}
Note that these comprehensions retain as much of the original container syntax as
possible. The list uses square brackets ([]
), the set uses curly braces ({}
), and
the dictionary uses curly braces {}
with keys and values separated by a colon (:
).
The upper_quarks
loop in the previous example can be thus transformed into the following single line:
upper_quarks
=
[
quark
.
upper
()
for
quark
in
quarks
]
Sometimes you might want to use a set comprehension instead of a list
comprehension. This situation arises when the result should have unique
entries but the expression may return duplicated values.
For example, if users are allowed to enter data that you know ahead of time is
categorical, then you can normalize the data inside of a set comprehension
to find all unique entries. Consider that users might be asked to enter quark
names, and lowercasing the entries will produce a common spelling. The following set
comprehension will produce a set of just
,
even though there are multiple spellings of the same quarks:{'top', 'charm', 'strange'}
entries
=
[
'top'
,
'CHARm'
,
'Top'
,
'sTraNGe'
,
'strangE'
,
'top'
]
quarks
=
{
quark
.
lower
()
for
quark
in
entries
}
It is also sometimes useful to write dictionary comprehensions. This often comes up
when you want to execute an expression over some data but also need to retain a
mapping from the input to the result. For instance,
suppose that we want to create a dictionary that maps numbers in an entries
list to the results of x**2 + 42
. This can be done with:
entries
=
[
1
,
10
,
12.5
,
65
,
88
]
results
=
{
x
:
x
**
2
+
42
for
x
in
entries
}
Comprehensions may optionally include a filter. This is a conditional
that comes after the iterable. If the condition evaluates to True
, then the
loop expression is evaluated and added to the list, set, or dictionary normally.
If the condition is False
, then the iteration is skipped. The syntax uses the if
keyword, as follows:
# List comprehension with filter
[
<
expr
>
for
<
loop
-
var
>
in
<
iterable
>
if
<
condition
>
]
# Set comprehension with filter
{
<
expr
>
for
<
loop
-
var
>
in
<
iterable
>
if
<
condition
>
}
# Dictionary comprehension with filter
{
<
key
-
expr
>
:
<
value
-
expr
>
for
<
loop
-
var
>
in
<
iterable
>
if
<
condition
>
}
Thus, list comprehensions with a filter are effectively shorthand for the following code pattern:
new_list
=
[]
for
<
loop
-
var
>
in
<
iterable
>
:
if
<
condition
>
:
new_list
.
append
(
<
expr
>
)
Suppose you had a list of words,
, that represented the entire text of
Principia Mathematica by Isaac Newton and you wanted to find all of the words,
in order, that started with the letter pm
. This operation could be performed
in one line with the following list comprehension with a filter:t
t_words
=
[
word
for
word
in
pm
if
word
.
startswith
(
't'
)]
Alternatively, take the case where you want to compute the set of squares of
Fibonacci numbers, but only where the Fibonacci number is divisible by five.
Given a list of Fibonacci numbers
, the desired set is computable via
this set comprehension:fib
{
x
**
2
for
x
in
fib
if
x
%
5
==
0
}
Lastly, dictionary comprehensions with filters are most often used to retain or remove items from another dictionary. This is often used when there also exists a set of “good” or “bad” keys. Suppose you have a dictionary that maps coordinate axes to indexes. From this dictionary, you only want to retain the polar coordinates. The corresponding dictionary comprehension would be implemented as follows:
coords
=
{
'x'
:
1
,
'y'
:
2
,
'z'
:
3
,
'r'
:
1
,
'theta'
:
2
,
'phi'
:
3
}
polar_keys
=
{
'r'
,
'theta'
,
'phi'
}
polar
=
{
key
:
value
for
key
,
value
in
coords
.
items
()
if
key
in
polar_keys
}
Comprehensions are incredibly
powerful and expressive. The reasoning goes that if the operation cannot fit into
a comprehension, then it should probably be split up into multiple lines in a
normal for
loop anyway. It is possible to nest comprehensions inside of one another,
just like loops may be nested. However, this can become pretty convoluted to
think about since two or more loops are on the same line. Python allows for simple
looping situations to be dealt with simply, and encourages complex loops to be made
readable.
Flow Control and Logic Wrap-up
Having reached the end of this chapter, you should be familiar with the following big ideas:
-
How to make decisions with
if-else
statements -
Handling the worst situations with exceptions
-
Reusing code with loops
-
The
for single in plural
loop pattern -
Using comprehensions to write concise loops
And now that you have seen the basics of decision making and code reuse, it is time to step those ideas up to the next level with functions in Chapter 5.
Get Effective Computation in Physics now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.