6. Logical Operations¶
6.1. The Identity Operator¶
Since all Python variables are actually object references, it sometimes makes
sense to ask whether two or more object references are referring to the same
object. The is
operator is a binary operator that returns True
if its left-hand
object reference is referring to the same object as its right-hand object reference.
Here are some examples:
>>> enz_1 = ("EcoR1", "Ecoli restriction enzyme I", "gaattc", 1, "sticky")
>>> enz_2 = ("EcoR1", "Ecoli restriction enzyme I", "gaattc", 1, "sticky")
>>> id(enz_1)
140454188035336
>>> id(enz_2)
140454188035240
>>> enz_1 is enz_2
False
>>> enz_1 == enz_2
True
>>> enz_2 = enz_1
>>> enz_1 is enz_2
True
Note that it usually does not make sense to use is
for comparing ints, strings, and
most other data types since we almost invariably want to compare their values.
In fact, using is
to compare data items can lead to unintuitive results, as we
can see in the preceding example, where, although enz_1
and enz_2
are initially set to
the same named tuple value, the named tuples themselves are held as separate objects and
is
therefore returns False
the first time we use it.
One benefit of identity comparison is that it is a very fast operation. The reason is that
the objects referred to do not have to be examined themselves. The is
operator
needs to compare only the memory addresses of the objects. The same address
means the same object.
The most common use case for is
is to compare a data item with the built-in
null object, None
, which is often used as a place-marking value to signify
“unknown” or “nonexistent”:
>>> a = "Something"
>>> b = None
>>> a is not None, b is None
(True, True)
To invert the identity test we use is not
.
The purpose of the identity operator is to see whether two object references
refer to the same object, or to see whether an object is None
. If we want to
compare object values we should use a comparison operator instead.
6.2. Comparison Operators¶
Python provides the standard set of binary comparison operators, with the expected semantics:
<
: less than<=
: less than or equal to==
: equal to!=
: not equal to>=
: greater than or equal to>
: greater than
These operators compare object values, that is, the objects that the object references used in the comparison refer to. Here are a few examples typed into a Python interactive interpreter:
>>> a = 2
>>> b = 6
>>> a == b
False
>>> a < b
True
>>> a <= b, a != b, a >= b, a > b
(True, True, False, False)
Everything is as we would expect with integers.
Strings are compared using lexicographic order (for normal letters, that’s the alphabetical order):
>>> a = "many paths"
>>> b = "many paths"
>>> a == b
True
>>> c = "mary patches"
>>> a < c
True
>>> d = "harry snatches"
>>> a < d
False
Warning
Be aware, though, that because Python 3 uses Unicode for representing strings, comparing strings that contain non-ASCII characters can be a lot subtler and more complicated than it might at first appear.
Warning
In some cases, comparing the identity of two strings or numbers, for instance,
using a is b
will return True
, even if each has been assigned separately as we
did here. This is because some implementations of Python will reuse the same
object (since the value is the same and the data type is immutable) for the sake of efficiency.
The moral of this is to use ==
and !=
when comparing values, and to use is
and
is not
only when comparing with None or when we really do want to see if two
object references, rather than their values, are the same.
One particularly nice feature of Python’s comparison operators is that they can be chained. For example:
>>> a = 9
>>> 0 <= a <= 10
True
This is a nicer way of testing that a given data item is in range than having to do two separate comparisons joined by logical and, as most other languages require. It also has the additional virtue of evaluating the data item only once (since it appears once only in the expression), something that could make a difference if computing the data item’s value is expensive, or if accessing the data item causes side effects.
Thanks to the “strong” aspect of Python’s dynamic typing, comparisons that don’t make sense will cause an exception to be raised. For example:
>>> "three" < 4
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: '<' not supported between instances of 'str' and 'int'
The same TypeError
exception would occur if we wrote "3" < 4
because Python does not try to guess
our intentions, the right approach is either to explicitly convert, for example,
int("3") < 4
, or to use comparable types, that is, both integers or both strings.
Python makes it easy for us to create custom data types that will integrate
nicely so that, for example, we could create our own custom numeric type
which would be able to participate in comparisons with the built-in int
type,
and with other built-in or custom numeric types, but not with strings or other
non-numeric types.
6.3. The Membership Operator¶
For data types that are sequences or collections such as strings, lists, and tuples,
we can test for membership using the in
operator, and for nonmembership
using the not in
operator. For instance:
>>> p = (4, "frog", 9, -33, 9, 2)
>>> 2 in p
True
>>> "dog" not in p
True
For lists and tuples, the in
operator uses a linear search which can be slow for
very large collections (tens of thousands of items or more). On the other hand,
in
is very fast when used on a dictionary or a set.
Here is how in can be used with a string:
>>> phrase = "Wild Swans by Jung Chang"
>>> "J" in phrase
True
>>> "han" in phrase
True
Conveniently, in the case of strings, the membership operator can be used to
test for substrings of any length. (A character is just a string
of length 1
.)
6.4. Boolean Operators¶
Python provides the three main operators of Boolean logic: and
, or
, and not
.
They behave very logically indeed:
>>> True and False
False
>>> True or False
True
>>> not False
True
Both and
and or
use short-circuit logic. This means that they only
evaluate what is necessary to determine the result (starting at the left
operand):
>>> True or (1 / 0)
True
>>> False or (1 / 0)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ZeroDivisionError: division by zero
In an expression like x or y
, if x
evaluates to True
, the whole
expression will also evaluate to True
.
It is therefore not necessary to evaluate the right operand in such a case.
However, if x
evaluates to False
, the result will depend on the value of y
.
In the above experiment, the evaluation of the right operand did only happen in the second case,
as revealed by the ZeroDivisionError
exception.
The same kind of reason explain the following behaviour:
>>> False and (1 / 0)
False
>>> True and (1 / 0)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ZeroDivisionError: division by zero
As soon as we know that the first operand of an and
expression evaluates to
False
, we know that the whole expression will evaluate to False
. The
evaluation of the right operand is therefore short-circuited in the first case.
When using non Boolean operands, the result is the operand that determined the result, not its conversion into a Boolean.
Let’s see how and
behaves:
>>> five = 5
>>> two = 2
>>> zero = 0
>>> five and two
2
>>> two and five
5
>>> five and zero
0
If the expression occurs in a Boolean context, the result is evaluated as a
Boolean, so the preceding expressions would come out as True
, True
, and False
in, say, an if
statement.
Now let’s test or
:
>>> nought = 0
>>> five or two
5
>>> two or five
2
>>> zero or five
5
>>> zero or nought
0
The or
operator is similar; here the results in a Boolean context would be True
,
True
, True
, and False
.
The not
unary operator evaluates its argument in a Boolean context and
always returns a Boolean result:
>>> not (zero or nought)
True
>>> not two
False