Now that we’ve seen all of Python’s built-in types, let’s take a quick look at some of the properties they share. Some of this section is a review of ideas we’ve already seen at work.
Table 2.11 classifies all the types we’ve seen, according to the type categories we introduced earlier. As we’ve seen, objects share operations according to their category—for instance, strings, lists, and tuples all share sequence operations. As we’ve also seen, only mutable objects may be changed in place. You can change lists and dictionaries in place, but not numbers, strings, or tuples.[17] Files only export methods, so mutability doesn’t really apply (they may be changed when written, but this isn’t the same as Python type constraints).
We’ve seen a number of compound object types (collections with components). In general:
Lists, dictionaries, and tuples can hold any kind of object.
Lists, dictionaries, and tuples can be arbitrarily nested.
Lists and dictionaries can dynamically grow and shrink.
Because they support arbitrary structures, Python’s compound object types are good at representing complex information in a program. For instance, the following interaction defines a tree of nested compound sequence objects; to access its components, we string as many index operations as required. Python evaluates the indexes from left to right, and fetches a reference to a more deeply nested object at each step. (This may be a pathologically complicated data structure, but it illustrates the syntax used to access nested objects in general.)
>>>L = ['abc', [(1, 2), ([3], 4)], 5]
>>>L[1]
[(1, 2), ([3], 4)] >>>L[1][1]
([3], 4) >>>L[1][1][0]
[3] >>>L[1][1][0][0]
3
We mentioned earlier
that
assignments always store references to objects,
not copies. In practice, this is usually what you want. But because
assignments can generate multiple references to the same object, you
sometimes need to be aware that changing a mutable object in place
may affect other references to the same object in your program. For
instance, in the following, we create a list assigned to
X
and another assigned to L
that embeds a reference back to list X
. We also
create a dictionary D
that contains another
reference back to list X
:
>>>X = [1, 2, 3]
>>>L = ['a', X, 'b']
>>>D = {'x':X, 'y':2}
At this point, there are three references to the list we created
first: from name X
, from the list assigned to
L
, and from the dictionary assigned to
D
. The situation is sketched in Figure 2.2.
Since lists are mutable, changing the shared list object from any of the three references changes what the other two reference:
>>>X[1] = 'surprise'
# changes all three references! >>>L
['a', [1, 'surprise', 3], 'b'] >>>D
{'x': [1, 'surprise', 3], 'y': 2}
One way to understand this is to realize that references are a higher-level analog of pointers in languages such as C. Although you can’t grab hold of the reference itself, it’s possible to store the same reference in more than one place
All Python objects also respond to the comparisons: test for equality, relative magnitude, and so on. Unlike languages like C, Python comparisons always inspect all parts of compound objects, until a result can be determined. In fact, when nested objects are present, Python automatically traverses data structures and applies comparisons recursively. For instance, a comparison of list objects compares all their components automatically:
>>>L1 = [1, ('a', 3)]
# same value, unique objects >>>L2 = [1, ('a', 3)]
>>>L1 == L2, L1 is L2
# equivalent?, same object? (1, 0)
Here, L1
and L2
are assigned
lists that are equivalent, but distinct objects. Because of the
nature of Python references, there are two ways to test for
equality:
==
operator tests value equivalencePython performs an equivalence test, comparing all nested objects recursively
is
operator tests object identityPython tests whether the two are really the same object (i.e., live at the same address).
In our example, L1
and L2
pass
the ==
test (they have equivalent values because
all their components are equivalent), but fail the
is
check (they are two different objects). As a
rule of thumb, the ==
operator is used in almost
all equality checks, but we’ll see cases of both operators put
to use later in the book. Relative magnitude comparisons are applied
recursively to nested data structures too:
>>>L1 = [1, ('a', 3)]
>>>L2 = [1, ('a', 2)]
>>>L1 < L2, L1 == L2, L1 > L2
# less, equal, greater: a tuple of results (0, 0, 1)
Here, L1
is greater than L2
because the nested 3
is greater than
2
. Notice that the result of the last line above
is really a tuple of three objects—the
results of the three expressions we typed (an example of a tuple
without its enclosing parentheses). The three values represent true
and false values; in Python as in C, an integer
represents false and an integer 1 represents
true. Unlike C, Python also recognizes any
empty data
structure as false and any nonempty data structure as true. Table 2.12 gives examples of
true
and false objects in Python.
Python also provides a special object called
None
(the last item in Table 2.12), which is always considered to be false.
None
is the only value of a special data type in
Python; it typically serves as an empty placeholder, much like a NULL
pointer in C. In general, Python compares the types we’ve seen
in this chapter, as follows:
Numbers are compared by relative magnitude.
Strings are compared lexicographically, character-by-character (“abc” < “ac”).
Lists and tuples are compared by comparing each component, from left to right.
Dictionaries are compared as though comparing sorted (key, value) lists.
In later chapters, we’ll see other object types that can change the way they get compared. For instance, class instances are compared by address by default, unless they possess special comparison protocol methods.
Finally, Figure 2.3 summarizes all the built-in object types available in Python and their relationships. In this chapter, we’ve looked at the most prominent of these; other kinds of objects in Figure 2.3 either correspond to program units (e.g., functions and modules), or exposed interpreter internals (e.g., stack frames and compiled code).
The main point we’d like you to notice here is that everything
is an object type in a Python system and may be processed by your
Python programs. For instance, you can pass a stack frame to a
function, assign it to a variable, stuff it into a list or
dictionary, and so on. Even types are an object type in Python: a
call to the built-in function type(X)
returns the
type object of object X
. Besides making for an
amazing tongue-twister, type objects can be used for manual type
comparisons in Python.
[17] You might think that number immutability goes without saying, but that’s not the case in every programming language. For instance, some early versions of FORTRAN allowed users to change the value of an integer constant by assigning to it. This won’t work in Python, because numbers are immutable; you can rest assured that 2 will always be 2.