That’s the end of our look at core data types. We’ll wrap up this part of the book with a discussion of common problems that seem to trap new users (and the occasional expert), along with their solutions. Some of this is a review of ideas we’ve already covered, but these issues are important enough to warn about again here.
Because this is such a central concept, I’ll mention it again: shared references to mutable objects in your program can matter. For instance, in the following example, the list object assigned to the name L is referenced both from L and from inside the list assigned to the name M. Changing L in place changes what M references, too:
>>>L = [1, 2, 3]>>>M = ['X', L, 'Y']# Embed a reference to L >>>M['X', [1, 2, 3], 'Y'] >>>L[1] = 0# Changes M too >>>M['X', [1, 0, 3], 'Y']
This effect usually becomes important only in larger programs, and shared references are often exactly what you want. If objects change out from under you in unwanted ways, you can avoid sharing objects by copying them explicitly. For lists, you can always make a top-level copy by using an empty-limits slice, among other techniques described earlier:
>>>L = [1, 2, 3]>>>M = ['X', L[:], 'Y']# Embed a copy of L (or list(L), or L.copy()) >>>L[1] = 0# Changes only L, not M >>>L[1, 0, 3] >>>M['X', [1, 2, 3], 'Y']
Remember, slice limits default to 0 and the length of the sequence being sliced; if both are omitted, the slice extracts every item in the sequence and so makes a top-level copy (a new, unshared object).
Repeating a sequence is like adding it to itself a number of times. However, when mutable sequences are nested, the effect might not always be what you expect. For instance, in the following example X is assigned to L repeated four times, whereas Y is assigned to a list containing L repeated four times:
>>>L = [4, 5, 6]>>>X = L * 4# Like [4, 5, 6] + [4, 5, 6] + ... >>>Y = [L] * 4# [L] + [L] + ... = [L, L,...] >>>X[4, 5, 6, 4, 5, 6, 4, 5, 6, 4, 5, 6] >>>Y[[4, 5, 6], [4, 5, 6], [4, 5, 6], [4, 5, 6]]
Because L was nested in the second repetition, Y winds up embedding references back to the original list assigned to L, and so is open to the same sorts of side effects noted in the preceding section:
>>>L[1] = 0# Impacts Y but not X >>>X[4, 5, 6, 4, 5, 6, 4, 5, 6, 4, 5, 6] >>>Y[[4, 0, 6], [4, 0, 6], [4, 0, 6], [4, 0, 6]]
This may seem artificial and academic—until it happens unexpectedly in your code! The same solutions to this problem apply here as in the previous section, as this is really just another way to create the shared mutable object reference case—make copies when you don’t want shared references:
>>>L = [4, 5, 6]>>>Y = [list(L)] * 4# Embed a (shared) copy of L >>>L[1] = 0>>>Y[[4, 5, 6], [4, 5, 6], [4, 5, 6], [4, 5, 6]]
Even more subtly, although Y doesn’t share an object with L anymore, it still embeds four references to the same copy of it. If you must avoid that sharing too, you’ll want to make sure each embedded copy is unique:
>>>Y[0][1] = 99# All four copies are still the same >>>Y[[4, 99, 6], [4, 99, 6], [4, 99, 6], [4, 99, 6]] >>>L = [4, 5, 6]>>>Y = [list(L) for i in range(4)]>>>Y[[4, 5, 6], [4, 5, 6], [4, 5, 6], [4, 5, 6]] >>>Y[0][1] = 99>>>Y[[4, 99, 6], [4, 5, 6], [4, 5, 6], [4, 5, 6]]
If you remember that repetition, concatenation, and slicing copy only the top level of their operand objects, these sorts of cases make much more sense.
We actually encountered this concept in a prior exercise: if a collection object contains a reference to itself, it’s called a cyclic object. Python prints a [...] whenever it detects a cycle in the object, rather than getting stuck in an infinite loop (as it once did long ago):
>>>L = ['grail']# Append reference to same object >>>L.append(L)# Generates cycle in object: [...] >>>L['grail', [...]]
Besides understanding that the three dots in square brackets represent a cycle in the object, this case is worth knowing about because it can lead to gotchas—cyclic structures may cause code of your own to fall into unexpected loops if you don’t anticipate them.
For instance, some programs that walk through structured data must keep a list, dictionary, or set of already visited items, and check it when they’re about to step into a cycle that could cause an unwanted loop. See the solutions to the Test Your Knowledge: Part I Exercises in Appendix D for more on this problem. Also watch for general discussion of recursion in Chapter 19, as well as the reloadall.py program in Chapter 25 and the ListTree class in Chapter 31, for concrete examples of programs where cycle detection can matter.
The solution is knowledge: don’t use cyclic references unless you really need to, and make sure you anticipate them in programs that must care. There are good reasons to create cycles, but unless you have code that knows how to handle them, objects that reference themselves may be more surprise than asset.
And once more for completeness: you can’t change an immutable object in place. Instead, you construct a new object with slicing, concatenation, and so on, and assign it back to the original reference, if needed:
T = (1, 2, 3) T[2] = 4 # Error! T = T[:2] + (4,) # OK: (1, 2, 4)
That might seem like extra coding work, but the upside is that the previous gotchas in this section can’t happen when you’re using immutable objects such as tuples and strings; because they can’t be changed in place, they are not open to the sorts of side effects that lists are.