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:

>>> from collections import namedtuple
>>> RestrictEnzyme = namedtuple("RestrictEnzyme", ("name", "comment", "sequence", "cut", "end"))
>>> enz_1 = RestrictEnzyme("EcoR1", "Ecoli restriction enzyme I", "gaattc", 1, "sticky")
>>> enz_2 = RestrictEnzyme("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