Learning Python - Mark Lutz [134]
Keys need not always be strings. Our examples so far have used strings as keys, but any other immutable objects (i.e., not lists) work just as well. For instance, you can use integers as keys, which makes the dictionary look much like a list (when indexing, at least). Tuples are sometimes used as dictionary keys too, allowing for compound key values. Class instance objects (discussed in Part VI) can also be used as keys, as long as they have the proper protocol methods; roughly, they need to tell Python that their values are hashable and won’t change, as otherwise they would be useless as fixed keys.
Using dictionaries to simulate flexible lists
The last point in the prior list is important enough to demonstrate with a few examples. When you use lists, it is illegal to assign to an offset that is off the end of the list:
>>> L = []
>>> L[99] = 'spam'
Traceback (most recent call last):
File " IndexError: list assignment index out of range Although you can use repetition to preallocate as big a list as you’ll need (e.g., [0]*100), you can also do something that looks similar with dictionaries that does not require such space allocations. By using integer keys, dictionaries can emulate lists that seem to grow on offset assignment: >>> D = {} >>> D[99] = 'spam' >>> D[99] 'spam' >>> D {99: 'spam'} Here, it looks as if D is a 100-item list, but it’s really a dictionary with a single entry; the value of the key 99 is the string 'spam'. You can access this structure with offsets much like a list, but you don’t have to allocate space for all the positions you might ever need to assign values to in the future. When used like this, dictionaries are like more flexible equivalents of lists. Using dictionaries for sparse data structures In a similar way, dictionary keys are also commonly leveraged to implement sparse data structures—for example, multidimensional arrays where only a few positions have values stored in them: >>> Matrix = {} >>> Matrix[(2, 3, 4)] = 88 >>> Matrix[(7, 8, 9)] = 99 >>> >>> X = 2; Y = 3; Z = 4 # ; separates statements >>> Matrix[(X, Y, Z)] 88 >>> Matrix {(2, 3, 4): 88, (7, 8, 9): 99} Here, we’ve used a dictionary to represent a three-dimensional array that is empty except for the two positions (2,3,4) and (7,8,9). The keys are tuples that record the coordinates of nonempty slots. Rather than allocating a large and mostly empty three-dimensional matrix to hold these values, we can use a simple two-item dictionary. In this scheme, accessing an empty slot triggers a nonexistent key exception, as these slots are not physically stored: >>> Matrix[(2,3,6)] Traceback (most recent call last): File " KeyError: (2, 3, 6) Avoiding missing-key errors Errors for nonexistent key fetches are common in sparse matrixes, but you probably won’t want them to shut down your program. There are at least three ways to fill in a default value instead of getting such an error message—you can test for keys ahead of time in if statements, use a try statement to catch and recover from the exception explicitly, or simply use the dictionary get method shown earlier to provide a default for keys that do not exist: >>> if (2,3,6) in Matrix: # Check for key before fetch ... print(Matrix[(2,3,6)]) # See Chapter 12 for if/else ... else: ... print(0) ... 0 >>> try: ... print(Matrix[(2,3,6)]) # Try to index ... except KeyError: # Catch and recover ... print(0) # See Chapter 33 for try/except ... 0 >>> Matrix.get((2,3,4), 0) # Exists; fetch and return 88 >>> Matrix.get((2,3,6), 0) # Doesn't exist; use default arg 0 Of these, the get method is the most concise in terms of coding requirements; we’ll study the if and try statements in more detail later in this book. Using dictionaries as “records” As you can see, dictionaries can play many roles in Python. In general, they can replace search data structures (because indexing by key is a search