To quote from Wikipedia's definition of control flow:
In computer science, control flow (or alternatively, flow of control) refers to the order in which the individual statements, instructions or function calls of an imperative or a declarative program are executed or evaluated.
We have already seen some methods of controling the flow:
However, there are various ways to affect the flow even further, thus achieving a finer control of the execution, a simpler or a more readable code, or a better error control.
We have already seen loops in Python:
for some_variable in range(arguments):
do_something
or
while some_condition:
do_something
Recall that the body of the loop is executed completely, i.e., the loop's condition gets checked only before the body's execution, not during.
So, how can we handle the following two cases in the middle of the body (i.e., not in the beginning or the end)?
Let us first define the function to compute the sum of the digits of a positive integer (to reduce the amount of typing later, as well as to make the code more readable):
In [1]:
def digitsum(n):
"""
Return the sum of the digits of `n`, which is assumed to be a positive integer.
"""
s = 0
while n > 0:
s += n % 10
n //= 10
return s
Let us now solve the problem using only what we've learned so far:
In [2]:
s = 0
last = False
while not last:
n = int(input("n = "))
if n == 0:
last = True
elif n > 0:
s += digitsum(n)
print("The sum of the digits of positive integers:", s)
Alternatively, we could have done this:
In [3]:
s = 0
n = int(input("n = "))
while n != 0:
if n > 0:
s += digitsum(n)
n = int(input("n = "))
print("The sum of the digits of positive integers:", s)
Notice that each of these simple examples is clumsy:
The first solution uses an extra variable last
and has most of the body wrapped in an if
or else
block.
The second solution also has most of the body wrapped in an if
or else
block.
Further, the body is badly organized: it first handles some n
loaded before and then loads a new version of n
(which naturally belongs to the next run of the body).
These may not seem like big issues, but do remember that this is just a simple example. Literally every part of the above code can be more complex:
input
could have been some complex data gathering (i.e., from the internet). Notice that, in the second case, we would either need to copy such a more complex loading twice in the code, or create a separate function (which could be messy if there is a big number of arguments to pass to such a function).
The conditions n != 0
and n > 0
could have been far more complex (for example, some data analysis).
We may have encountered far more than just two simple conditions, which could have easily lead to very deep indentation (if
inside if
inside if
inside...).
So how to do it better?
Let us now rewrite it a bit (but still in English), to better resemble a program structure:
Repeat this:
Load
n
.
Ifn == 0
, break this loop.
Ifn < 0
, continue this loop (i.e., skip the rest of the body and go back to loading a number).
Increases
by the sum of digits ofn
.s
.
Python has statements break
and continue
that behave exactly the way our "English code" expects them to:
break
-- break the loop (i.e., continue the execution on the first command after the loop's body),
continue
-- stop the current execution of the body, but don't exit the loop (i.e., continue the execution on the loop's header).
The only question is how to handle "repeat this". Notice that this is equivalent to "repeat this unconditionally", which in Python is written as
while True:
...
In [4]:
s = 0
while True:
n = int(input("n = "))
if n == 0:
break
if n < 0:
continue
s += digitsum(n)
print("The sum of the digits of positive integers:", s)
Observe how the code is now naturally organized and easy to read:
while True
, a so called infinite loop, tells us that the execution will be terminated somewhere in the body, most likely depending on the input. This means that the final input will probably be ignored. Otherwise, we'd break the loop after the body's execution, i.e., using an appropriate loop condition.
The first thing we do in the body is load the data that will be processed in that execution of the body.
We have some condition that leads to break
ing, which means that this is the condition to stop.
If we had more than one such condition, we could have easily written
if condition1:
break
if condition2:
break
...
which would be a very easy to read sequance of obviously breaking conditions.
break
and continue
exist in most of the modern languages (including Python 2) and have the same functionality.
"Infinite" loops are not directly related to either break
or continue
.
However, remember the (somewhat informal) definition of an algorithm:
A sequence of actions that are always executed in a **finite** number of steps, used to solve a certain problem.
It is very important to always terminate your loops, be it via their conditions or a break
statement or some other breaking technique.
The break
and continue
affect the innermost loop. In other words, in the following code
while condition1:
...
while condition2:
...
if condition3:
break
... # this is skipped by break
... # break "jumps" here
... # not here
the break
statement will break only the inner while
loop (the one with a condition condition2
).
The break
and continue
have no meaning outside of the loops (for example, in an if
block that is not inside of some loop). Using them there will cause a syntax error (SyntaxError: 'break' outside loop) as soon as you try to run your program. Even this will not work:
In [5]:
def f():
break
for i in range(17):
f()
In [6]:
lst = list() # Create an empty list
not_done = True
while not_done:
x = int(input("Please type an integer: "))
if x == -17:
not_done = False
else:
lst.append(x)
print("The sorted list:", sorted(lst, reverse=True))
However, using a break
is much more natural here:
In [7]:
lst = list() # Create an empty list
while True:
x = int(input("Please type an integer: "))
if x == -17:
break
lst.append(x)
print("The sorted list:", sorted(lst, reverse=True))
Similarly, if we want to keep $-17$ in the list, we just append the new number before checking if the loop should stop:
In [8]:
lst = list() # Create an empty list
while True:
x = int(input("Please type an integer: "))
lst.append(x)
if x == -17:
break
print("The sorted list:", sorted(lst, reverse=True))
else
with loopsAs we have mentioned before, it is possible to use else
in conjuction with loops in Python.
The body of a loop's else
is executed if the loop was not terminated by a break
statement (but by its condition).
Observe the following example (try running it twice: once with loading a zero, and once with loading a negative number):
In [9]:
n = 17
while n > 0:
n = int(input("n = "))
if n == 0:
break
else:
print("The loop was terminated with a negative number (not a zero!).")
Warning: If you use this, be careful with your indentation! The above is not the same as
In [10]:
n = 17
while n > 0:
n = int(input("n = "))
if n == 0:
break
else:
print("The loop was terminated with a negative number (not a zero!).")
This one we already know, but it is worth mentioning it here as well:
return
returns a value from the function, but it also immediatelly terminates the function's execution.
This makes it very convenient to do various tasks without using some extra variables. Let us observe an example:
In [11]:
n = int(input("n = "))
if n < 2:
is_prime = False
else:
is_prime = True
if is_prime:
for d in range(2, n):
if n % d == 0:
is_prime = False
break
if is_prime:
print("The number", n, "is a prime.")
else:
print("The number", n, "is not a prime.")
A somewhat shorter (albeit a bit more cryptic) version:
In [12]:
n = int(input("n = "))
is_prime = (n >= 2)
if is_prime:
for d in range(2, n):
if n % d == 0:
is_prime = False
break
if is_prime:
print("The number", n, "is a prime.")
else:
print("The number", n, "is not a prime.")
Let us now observe how this can be done with a function:
In [13]:
def is_prime(n):
"""
Return True if `n` is a prime; False otherwise.
"""
if n < 2: return False
for d in range(2, n):
if n % d == 0:
return False
return True
n = int(input("Input an integer: "))
if is_prime(n):
print("The number", n, "is a prime.")
else:
print("The number", n, "is not a prime.")
Observe the natural organization of the function code:
If n < 2
, we know that n
is not a prime, so stop analyzing it and just return False
.
Check the potential divisors ($2, 3, \dots, n-1$). If any of them divides n
, we have found out that n
is not a prime, so stop analyzing it and just return False
.
After the loop, if we are still here, that means that we found no divisors (otherwise, the return
in the loop would stop the function's execution), which means that the number is a prime, so return True
.
Note: A more Pythonic way to do the above would be:
In [14]:
def is_prime(n):
"""
Return True if `n` is a prime; False otherwise.
"""
return (n > 1) and all(n % d > 0 for d in range(2, n))
n = int(input("Input an integer: "))
if is_prime(n):
print("The number", n, "is a prime.")
else:
print("The number", n, "is not a prime.")
We have seen how to break loops and how to break functions. In those terms, exceptions could be regarded as break anything (no matter how deep).
The basic use of exceptions, written in plain English, is like this:
Do this:
Some work (may include loops, function calls, more exceptions,... anything!)
If something "unexpected" happened,
Handle it like this.
The simpliest example of an exception is a division by zero. See what happens if you try to do it in the following simple program that loads a
and b
and computes $\sqrt{a/b}$.
In [15]:
from math import sqrt
a = float(input("a = "))
b = float(input("b = "))
print("sqrt({} / {}) = {}".format(a, b, sqrt(a/b)))
Notice the last line?
ZeroDivisionError: float division by zero
This is an unhandled exception. What happened is that a/b
produced an error which then exits the current block of execution (be it if
, for
, a function,...), then the one that's surrounding it, and so on, until it is either handled or it exists the program.
Since we didn't handle that exception, the program was terminated, with a rather ugly (but informative!) message.
See what happens when we handle an exception:
In [16]:
from math import sqrt
try:
a = float(input("a = "))
b = float(input("b = "))
print("sqrt({} / {}) = {}".format(a, b, sqrt(a/b)))
except Exception:
print("Your a and/or b are wrong.")
It is possible to make a general exception
block that catches all the exceptions:
try:
...
except:
...
However, never do that, as it masks the errors you did not anticipate and disables breaking the program with Ctrl+C
and similar mechanisms.
At minimum, use except Exception
. However, it is much better to catch more specific exception(s) when possible.
Notice how both (a,b) = (1,0)
and (a,b) = (-1,1)
will give you the same error (that your a and/or b are wrong), even though these are different errors: the first input causes the divison by zero, while the second one contains the real square root of a negative number. So, how do we distinguish between these two?
Notice the ZeroDivisionError
above? This is the name of the exception that has happened. We can specify it, so that our try...except
block handles exactly this error (while leaving all other errors unhandled):
In [17]:
from math import sqrt
try:
a = float(input("a = "))
b = float(input("b = "))
print("sqrt({} / {}) = {}".format(a, b, sqrt(a/b)))
except ZeroDivisionError:
print("Division by zero.")
What happens if you load (a,b) = (-1,1)
in the above code?
To handle more than one exception, we can just add more except
blocks (using the names of the appropriate exceptions):
In [18]:
from math import sqrt
try:
a = float(input("a = "))
b = float(input("b = "))
print("sqrt({} / {}) = {}".format(a, b, sqrt(a/b)))
except ZeroDivisionError:
print("Division by zero.")
except ValueError:
print("a/b does not have a real square root!")
In [19]:
from cmath import sqrt
try:
a = float(input("a = "))
b = float(input("b = "))
result = sqrt(a/b)
if result.imag > 0:
print("sqrt({} / {}) = {}i".format(a, b, result.imag))
else:
print("sqrt({} / {}) = {}".format(a, b, result.real))
except ZeroDivisionError:
print("Division by zero.")
except ValueError:
print("a/b does not have a square root!")
Note the following two details:
The imaginary unit in Python is displayed as j
(instead of i
), which is the reason why we handled the two cases ourselves instead of directly printing the result.
The except ValueError
block in the above code is useless, as there is no input that can cause a ValueError
with a complex square root.
In [20]:
from math import sqrt
try:
a = float(input("a = "))
b = float(input("b = "))
result = sqrt(a/b)
except ZeroDivisionError:
print("Division by zero.")
except ValueError:
print("a/b does not have a square root!")
else:
print("sqrt({} / {}) = {}".format(a, b, result))
This is very similar to the previuous code (the last one without an else
part). Can you spot in what way do the two codes differ in their behaviour?
In [21]:
from math import sqrt
a = float(input("a = "))
b = float(input("b = "))
if b == 0:
print("Division by zero.")
else:
result1 = a/b
if result1 < 0:
print("a/b does not have a real square root!")
else:
result2 = sqrt(result1)
print("sqrt({} / {}) = {}".format(a, b, result2))
Obviously, the code is not very nice. A few more checks, and the indentation would get quite messy. Notice that there is an easy way around that by using a function (how?).
However, while the above code just got a bit "uglier" without exceptions, it is far more often that it would be much harder or even impossible to go without them. Consider the following, trivial yet very real example:
Make sure that the user inputs proper real numbers. If they don't, do not crash the program, but warn the user and ask them to try again.
One way to deal with this problem is:
Load a string with
s = input()
and then check ifs
contains a real number. If it does, convert it withx = float(s)
; otherwise, report an error and go back to the input.
This seems simple enough. However, the user's input is never to be trusted. It can contain anything that is possible to type on the keyboard (and much, much more!), due to typos, errors, malicious intent, or some other reason.
If you try to write a code that will check if the input contains a valid "real" number, you'll see that it's not an easy thing to do (it gets easier with regular expressions, but it's still not trivial). Here is what you'd have to consider:
+
),-17.19
) and scientific (example: -1.719e1
) format, with or without the exponent's sign.So, while checking that the string "-17.19"
can be converted to a (real) number seems easy, doing the same for " \n \t -1.719e+1\f"
, while still disallowing any illegal inputs (like, for example, " \n \t -1.719e+1\f1"
or "1719e1.1"
), is much harder.
And all of this for just a simple input of a single real number!
Using the exceptions, this gets trivial:
In [22]:
while True:
try:
x = float(input("Input a real number: "))
except ValueError:
print("Shame on you! Don't you know what a real number is?!? :-P")
else:
break
print("Your real number:", x)
Note: When writing "real programs" (i.e., not just "school examples" or small utilities that only you will be using), you should always check that the input is valid. If it is not, handle it appropriately (print a message, request the input again, stop the program,... whatever you deem fit for that specific situation).
finally
= "wrap it up"One can also add a finally
block to an exception handler:
try:
do_something_1()
do_something_2()
except SomeException:
handle_exception()
else:
no_error()
finally:
wrap_it_all_up()
Here is what happens in some possible scenarios:
If no error occurs:
do_something_1()
do_something_2()
no_error()
wrap_it_all_up()
If SomeException
occurs at the end of do_something_1()
:
do_something_1()
handle_exception()
wrap_it_all_up()
If SomeOtherException
occurs at the end of do_something_1()
:
do_something_1()
wrap_it_all_up()
and SomeOtherException
is still raised (as if raise SomeOtherException(...)
was called after wrap_it_all_up()
).
If it exists, finally
block is always executed before leaving the try
block.
In [23]:
n = int(input("x = "))
for i in range(n):
try:
if i*i == n:
print(n, "is a complete square of", i)
break
finally:
print("This is executed for i =", i)
However, finally
itself catches no exceptions. For examle:
In [24]:
n = int(input("x = "))
for i in range(n):
try:
if i*i == n:
print(n, "is a complete square of", i)
raise Exception
finally:
print("This is executed for i =", i)
In [25]:
def f(n, f, t):
for i in range(1, n):
for j in range(f, t):
print("{} / {} = {}".format(i, j, i / j))
n = int(input("n = "))
if n % 2 == 0:
print("The number is even, so let's call this function without handling any exceptions:")
f(n, -n, n)
else:
print("The number is odd, so let's call this function:")
try:
if n > 7:
f(n, -n, n)
else:
f(n, 1, n)
except ZeroDivisionError:
print("Whoops... You really do like dividing by zero!")
Exceptions are in no way something magical that just happens. You can trigger them yourself, either to report an error or to achieve a fine control of your code's execution.
The correct term for triggering an exception is raising it, which is done with the keyword raise
(other modern languages will use either raise
or throw
).
Consider the following problem:
Write a function that takes an (integer) argument
n
, loadsn
real numbers, and returns the arithmetic mean of the loaded numbers.
This one should be trivial to write:
In [26]:
def arithmetic_mean(n):
"""
Load `n` real numbers and return the arithmetic mean of the loaded numbers.
"""
sum_ = 0
for i in range(n):
x = int(input("Number #{}: ".format(i+1)))
sum_ += x
return sum_ / n
print("Result:", arithmetic_mean(3))
This code will execute as expected. However, there is a designer flaw in it. Can you spot it?
What happens if we do this?
In [27]:
print("Result:", arithmetic_mean(0))
One could argue that the input is faulty and makes no sense, hence the error is expected. While this is true, and the code should do this, the error itself is confusing:
ZeroDivisionError: division by zero
What division? The user of our function knows that they have to get an arithmetic mean, but they need not know how it is calculated. Imagine if the problem was more complex than a simple arithmetic mean: it would be perfectly OK for a user to not know the internal functioning of a function that you wrote.
Further, this will report no error:
In [28]:
print("Result:", arithmetic_mean(-17))
This result is a nonsense, in a way that an arithmetic mean is not defined for a negative number of numbers (actually, "-17 numbers" is a nonsense itself). One could argue that the result $0$ makes some sense, but it is certainly different from the arithmetic mean of, for example, {-17,-2,19}
. The user should be made aware of that.
So, how to deal with this?
Traditionally, the function would return an invalid value. For example, if we were computing the sum of digits, $-1$ would be an invalid result. However, there is no "invalid arithmetic mean" -- every real number is an arithmetic mean of an infinite number of sets of real numbers.
We could return None
, but this solution is bad because it doesn't really imply an error. Unless the user checks specifically for None
, they might never realize that there was an error. Any value can be compared with None
(the result will be False
, unless that value is also None
). Also, returning None
or its equivalent will not work in strongly typed languages (C++, for example).
The proper way to do this is to raise your own exception.
First we need to determine the type of an exception, so that the exception properly describes what has happened. We can always define our own exceptions, but usually we can pick among the existing ones.
Here, the problem is in the wrong value of n
, which fits perfectly in the description of the ValueError
type:
exception
ValueError
Raised when a built-in operation or function receives an argument that has the right type but an inappropriate value, and the situation is not described by a more precise exception such as
IndexError
.
We are now ready to go:
In [29]:
def arithmetic_mean(n):
"""
Load `n` real numbers and return the arithmetic mean of the loaded numbers.
If `n` has an illegal value (i.e., `n <= 0`), the function rises a ValueError exception.
"""
if n <= 0:
raise ValueError("Arithmetic mean of zero or less numbers is undefined")
sum_ = 0
for i in range(n):
x = int(input("Number #{}: ".format(i+1)))
sum_ += x
return sum_ / n
print("Result:", arithmetic_mean(3))
The previous illegal value example will now raise an exception:
In [30]:
print("Result:", arithmetic_mean(-17))
We can now make a call like this, to report our own error message:
In [31]:
n = int(input("n = "))
try:
print("Result:", arithmetic_mean(n))
except ValueError:
print("The arithmetic mean is well defined only when n > 0")
or like this, to use the one defined in the function:
In [32]:
n = int(input("n = "))
try:
print("Result:", arithmetic_mean(n))
except ValueError as e:
print(e)
or like this, to treat "bad input" as if the result was zero:
In [33]:
n = int(input("n = "))
try:
am = arithmetic_mean(n)
except ValueError:
am = 0
print("Result:", am)
In [34]:
try:
x = 0 / 0
except ZeroDivisionError:
print("We're OK!")
This leads us to...
A favourite activity of many people, doing nothing, is implemented in Python as the pass
statement. It literally means "do nothing" (so, it's not a part of control flow in the strict sense) and it is used when the syntax requires at least one command, but there is none to be executed.
Here is an example:
In [35]:
n = int(input("n = "))
if n > 17:
pass
else:
print("Your number is smaller than or equal to 17.")
Without pass
, this would fail:
In [36]:
n = int(input("n ="))
if n > 17:
else:
print("Your number is smaller than or equal to 17.")
Obviously, there is a better way to do the above:
In [37]:
n = int(input("n ="))
if n <= 17:
print("Your number is smaller than or equal to 17.")
However, there are legitimate situations when we need to have empty blocks.
One such example can be "I need to add some code here, but I'll do it later":
if some_condition:
pass # TODO: write the code to solve the problem if some_condition happens
else:
a_general_solution()
Another example is defining our own exceptions:
class SomeNewError(Exception):
pass
All we need here is a new type of exception (so it can be distinguished from other exceptions), but with no new functionality. This can now be used the same way we used ValueError
above:
In [38]:
class SomeNewError(Exception):
"""
Description of the error (never forget docstrings when defining something!)
"""
pass
raise SomeNewError("This is a very informative message")
While the word "Error
" in the name "SomeNewError
" is not mandatory, it is a Python standard to use it for all exceptions that describe some error (see Exception Names in PEP 8).
The statement pass
can also give us a solution to the question about handling exceptions silently:
In [39]:
try:
x = 0 / 0
except ZeroDivisionError:
pass
print("We're OK!")
Note that it is usually desirable to not just ignore an error, but it can come in handy.
In [40]:
class BreakException(Exception):
pass
n = int(input("n = "))
m = int(input("m = "))
for i in range(n):
print("\ni =", i)
try:
for j in range(n):
for k in range(n):
s = i+j+k
print("i + j + k = {} + {} + {} = {}".format(i, j, k, s))
if s == m:
print("Stop!")
raise BreakException()
except BreakException:
pass
Note: The name BreakException
is just something we have chosen. It can be called any way we like, but it is highly desirable to make it descriptive.
Note that the code can almost always be refactored (i.e., rewritten) by converting its parts to functions and then breaking loops with return
. For the above code, it would look like this:
In [41]:
n = int(input("n = "))
m = int(input("m = "))
def inner_loops(i):
"""
Inner loops that sometimes need to be interrupted all at once.
We are using `return` to achieve that effect.
"""
for j in range(n):
for k in range(n):
s = i+j+k
print("i + j + k = {} + {} + {} = {}".format(i, j, k, s))
if s == m:
print("Stop!")
return
for i in range(n):
print("\ni =", i)
inner_loops(i)