2023-01-23
So far in these lessons we have seen a variety of data types: int, float, complex, list, array and string. The bool type can take only two possible values, True and False, and is useful whenever you want to execute a group of statements selectively, based on whether or not a condition is true.
The aim of this lesson is to learn the basic logical operations that Python provides for bool variables, and use them in some simple control flow constructions. By the end of the lesson, you should know how to
<) to compare different values;The table below list the commonly-used relational operators in Python.
| Relation | Description | Example |
|---|---|---|
< |
less than | 2 < 3 is True |
<= |
less than for equal to | 2 <= 3 is True |
> |
greater than | 2 > 3 is False |
>= |
greater than or equal to | 2 >= 3 is False |
== |
equal to | 2 == 3 is False |
!= |
not equal to | 2 != 3 is True |
If a and b are arrays of the same shape, then the relational operators act elementwise. Thus,
a = array([[3, 7],
[2, -8]])
b = array([[-6, 0],
[ 4, 9]])
print(a > b)
produces
[[ True True]
[False False]]
You can also compare an array to a scalar:
print(a > 2)
produces
[[ True True]
[False False]]
The following truth table defines the Boolean operator and, following the usual rules of logic.
A |
B |
A and B |
|---|---|---|
True |
True |
True |
True |
False |
False |
False |
True |
False |
False |
False |
False |
Likewise, the operator or has its usual meaning.
A |
B |
A or B |
|---|---|---|
True |
True |
True |
True |
False |
True |
False |
True |
True |
False |
False |
False |
Finally, the unary Boolean operator not is defined in the obvious way.
A |
not A |
|---|---|
True |
False |
False |
True |
We can use these operators in combination with the comparison operators to build up logical expressions. The comparison operators have the highest priority, followed by not, with and and or having the lowest priority. The expressions are evaluated from left to right. In practice, it is best to use parentheses to make the meaning clear.
Exercise. Work out the truth value for each of the following expressions, and check your answers by typing them in the IPython shell. (Remember that m % n is the remainder when m is divided by n.)
(7 < 5) or not(6 > 2)
(2 > 1) and ( 16 % 2 == 0 )
( 1 + 1 == 2 ) and ( not( 9 % 4 == 1 ) or ( e < pi ) )
Both and and or are short-circuit operators, that is, the right operand is never evaluated if the truth value can be determined from only the left operand. Thus, if ‘A’ is False then ‘A and B’ must be False, regardless of whether ‘B’ is True or False, so there is no point in evaluating ‘B’. Similarly, if ‘A’ is True, then ‘A or B’ must be True regardless of whether ‘B’ is True or False, so again there is no point in evaluating ‘B’. For example, following the statements
x = -2
y = nan
(x > 0) and (y := log(x))
the value of y will be nan. Indeed, y will be nan if x has been assigned any value less than or equal to zero, whereas y will be log(x) if x has been assigned a positive value. Notice that we have to use the walrus operator := if an assignment is part of a larger expression.
In Python, the basic if-construct has the form
if <condition>:
<statements>
where <condition> is a logical expression and <statements> consists of one or more lines of code, all indented by the same number of spaces. If <condition> evaluates to True, then the statements are executed. Otherwise, i.e., if <condition> evaluates to False, the statements are skipped and the thread of control passes to the next (unindented) line of code. Note that a colon is necessary following <condition>.
A longer form of the construct is
if <condition1>:
<statements1>
elif <condition2>:
<statements2>
else:
<statements3>
In this case,
<statements1> is executed iff <condition1> is True;<statement2> is executed iff <condition1> is False and <condition2> is True;<statement3> is executed iff <condition1> and <condition2> are both False.Multiple elif clauses are also permitted. Recall the construction involving the walrus operator := at the end of the last section: the same effect can be achieved by
if x > 0:
y = log(x)
else:
y = nan
In Lesson 4, we defined a function solve_quadratic. By using if-constructs, we can improve this function so that it handles the case when the discriminant turns out to be negative, leading to complex conjugate roots.
def solve_quadratic(a, b, c):
"""Solve the quadratic equation ax^2 + bx + c = 0.
Assumes that the coefficients a, b, c are all real,
and that a is not zero."""
dscr = b**2 - 4*a*c
if dscr > 0: # Distinct real roots
sqrt_dscr = sqrt(dscr)
x_plus = ( -b + sqrt_dscr ) / ( 2 * a )
x_minus = ( -b - sqrt_dscr ) / ( 2 * a )
elif dscr < 0: # Complex conjugate roots
sqrt_abs_dscr = sqrt(-dscr)
x_plus = complex(-b, sqrt_abs_dscr) / ( 2 * a )
x_minus = complex(-b, -sqrt_abs_dscr) / ( 2 * a )
else: # Equal real roots
x_plus = x_minus = -b / ( 2 * a )
return x_plus, x_minus
Notice that we have inserted comments in the source code to help document the code. The Python interpreter will ignore any text you insert following a ‘#’ character, up to the end of the current line.
Exercise. If \(|4ac|\) is very small compared to \(b^2\), then dscr will be approximately equal to \(|b|\), and so one of the two real roots can suffer from loss of precision due to cancellation of leading digits. How could you improve the code to avoid this problem? Hint: the product of the roots equals \(c/a\).
Exercise. The solve_quadratic function assumes that a is not 0, otherwise we do not have a quadratic equation but just a linear equation \(bx+c=0\) which has only one root \(x=-c/b\) (assuming \(b\) is not zero). Can you think of any other problematic cases? The usual way to handle an ‘unreasonable’ value of a function argument is to raise a ValueError. The syntax is as follows.
raise ValueError('Leading coefficient must be non-zero.')
In Python, a for-loop has the form
for k in <iterable>:
<statements(k)>
Here, k is called the loop variable and <statments(k)> consists of one or more lines of code, each indented by the same amount and possibly depending on k. These statements are called the loop body. If <iterable> is a list, then <statements(k)> is executed with k replaced by each item of the list in order. For example,
for k in ['Verity', 'Gavin', 'Eunice']:
print(k)
produces
Verity
Gavin
Eunice
Instead of a list, we can use an array.
for k in arange(4):
print(f'The square of {k} is {k**2}')
produces
The square of 0 is 0
The square of 1 is 1
The square of 2 is 4
The square of 3 is 9
Python also provides a function range that has the same effect as arange in the for-loop but generates the values of the loop variable on-the-fly without creating a list or array. In this way, range(n) uses much less memory than arange(n) if n is large.
When the loop variable is a number, it is often called the loop counter.
Often, a for-loop can be replaced with a NumPy operation. For example, suppose that we wanted to sum \(1/k^2\) for \(k\) from \(1\) to \(1000\). We could do
s = 0.0
for k in range(1, 1001):
s += 1 / k**2
print(f'The value of the sum is {s}')
or alternatively, using the sum method of an array,
terms = 1 / arange(1, 1001)**2
print(f'The value of the sum is {terms.sum()}')
Because the NumPy calculations are performed using native CPU instructions they will likely be one to two orders of magnitude faster than the Python for-loop, at the expense of the extra computer memory needed to store the vector terms.
In this lesson, we have seen how to
For a more complete treatment of the topics in this lesson, see the section 4. More Control Flow Tools from the Tutorial in the official Python documentation.