How to Think Like a Computer Scientist: 12. Dictionaries

# 12. Dictionaries

All of the compound data types we have studied in detail so far---strings, lists, and tuples---are sequence types, which use integers as indices to access the multiple values they contain.

Dictionaries are a different kind of compound type. They are Python's mapping type. They map keys which can be of any immutable type to values which can be of any type, just like the values of a list or tuple.

As an example, we will create a dictionary to translate English words into Spanish. For this dictionary, the keys are strings.

One way to create a dictionary is to start with the empty dictionary and add key-value pairs. The empty dictionary is denoted `{}`:

    >>> eng2sp = {}
    >>> eng2sp['one'] = 'uno'
    >>> eng2sp['two'] = 'dos'

The first assignment creates a dictionary named `eng2sp`; the other assignments add new key-value pairs to the dictionary. We can print the current value of the dictionary in the usual way:

    >>> print eng2sp
    {'two': 'dos', 'one': 'uno'}

The key-value pairs of the dictionary are seperated by commas. Each pair contains a key and a value separated by a colon.

The order of the pairs may not be what you expected. Python uses complex algorithms to determine where the key-value pairs are stored in a dictionary. For our purposes we can think of this ordering as unpredicatable.

Another way to create a dictionary is to provide a list of key-value pairs using the same syntax as the previous output:

    >>> eng2sp = {'one': 'uno', 'two': 'dos', 'three': 'tres'}

It doesn't matter what order we write the pairs. The values in a dictionary are accessed with keys, not with indices, so there is no need to care about ordering.

Here is how we use a key to look up the corresponding value:

    >>> print eng2sp['two']
    'dos'

The key `'two'` yields the value `'dos'`.

## 12.1 Dictionary operations

The `del` statement removes a key-value pair from a dictionary. For example, the following dictionary contains the names of various fruits and the number of each fruit in stock:

    >>> inventory = {'apples': 430, 'bananas': 312, 'oranges': 525, 'pears': 217}
    >>> print inventory
    {'oranges': 525, 'apples': 430, 'pears': 217, 'bananas': 312}

If someone buys all of the pears, we can remove the entry from the dictionary:

    >>> del inventory['pears']
    >>> print inventory
    {'oranges': 525, 'apples': 430, 'bananas': 312}

Or if we're expecting more pears soon, we might just change the value associated with pears:

    >>> inventory['pears'] = 0
    >>> print inventory
    {'oranges': 525, 'apples': 430, 'pears': 0, 'bananas': 312}

The `len` function also works on dictionaries; it returns the number of key-value pairs:

    >>> len(inventory)
    4

## 12.2 Dictionary methods

Dictionaries have a number of useful built-in methods

The `keys` method takes a dictionary and returns a list of the keys that appear, but instead of the function syntax `keys(eng2sp)`, we use the method syntax `eng2sp.keys()`.

    >>> eng2sp.keys()
    ['one', 'three', 'two']

This form of dot notation specifies the name of the method, `keys`, and the name of the object on which to apply the method, `eng2sp`. The parentheses indicate that this method takes no parameters.

A method call is called an invocation; in this case, we would say that we are invoking the `keys` method on the object `eng2sp`. As we will see in a few chapters when we talk about object oriented programming, the object on which a method is invoked is actually the first argument to the method.

The `values` method is similar; it returns a list of the values in the dictionary:

    >>> eng2sp.values()
    ['uno', 'tres', 'dos']

The `items` method returns both, in the form of a list of tuples---one for each key-value pair:

    >>> eng2sp.items()
    [('one','uno'), ('three', 'tres'), ('two', 'dos')]

The syntax provides useful type information. The square brackets indicate that this is a list. The parentheses indicate that the elements of the list are tuples.

If a method takes an argument, it uses the same syntax as a function call. For example, the method `has_key` takes a key and returns true (1) if the key appears in the dictionary:

    >>> eng2sp.has_key('one')
    True
    >>> eng2sp.has_key('deux')
    False

If you try to call a method without specifying an object, you get an error. In this case, the error message is not very helpful:

    >>> has_key('one')
    NameError: name 'has_key' is not defined

## 12.3 Aliasing and copying

Because dictionaries are mutable, you need to be aware of aliasing. Whenever two variables refer to the same object, changes to one affect the other.

If you want to modify a dictionary and keep a copy of the original, use the `copy` method. For example, `opposites` is a dictionary that contains pairs of opposites:

    >>> opposites = {'up': 'down', 'right': 'wrong', 'true': 'false'}
    >>> alias = opposites
    >>> copy = opposites.copy()

`alias` and `opposites` refer to the same object; `copy` refers to a fresh copy of the same dictionary. If we modify `alias`, `opposites` is also changed:

    >>> alias['right'] = 'left'
    >>> opposites['right']
    'left'

If we modify `copy`, `opposites` is unchanged:

    >>> copy['right'] = 'privilege'
    >>> opposites['right']
    'left'

## 12.4 Sparse matrices

We previously used a list of lists to represent a matrix. That is a good choice for a matrix with mostly nonzero values, but consider a sparse matrix like this one:

The list representation contains a lot of zeroes:

    matrix = [ [0,0,0,1,0],
       [0,0,0,0,0],
       [0,2,0,0,0],
       [0,0,0,0,0],
       [0,0,0,3,0] ]

An alternative is to use a dictionary. For the keys, we can use tuples that contain the row and column numbers. Here is the dictionary representation of the same matrix:

    matrix = {(0,3): 1, (2, 1): 2, (4, 3): 3}

We only need three key-value pairs, one for each nonzero element of the matrix. Each key is a tuple, and each value is an integer.

To access an element of the matrix, we could use the `[]` operator:

    matrix[(0, 3)]
    1

Notice that the syntax for the dictionary representation is not the same as the syntax for the nested list representation. Instead of two integer indices, we use one index, which is a tuple of integers.

There is one problem. If we specify an element that is zero, we get an error, because there is no entry in the dictionary with that key:

    >>> matrix[(1, 3)]
    KeyError: (1, 3)

The `get` method solves this problem:

    >>> matrix.get((0, 3), 0)
    1

The first argument is the key; the second argument is the value `get` should return if the key is not in the dictionary:

    >>> matrix.get((1, 3), 0)
    0

`get` definitely improves the semantics of accessing a sparse matrix. Shame about the syntax.

## 12.5 Hints

If you played around with the `fibonacci` function from the last chapter, you might have noticed that the bigger the argument you provide, the longer the function takes to run. Furthermore, the run time increases very quickly. On one of our machines, `fibonacci(20)` finishes instantly, `fibonacci(30)` takes about a second, and `fibonacci(40)` takes roughly forever.

To understand why, consider this call graph for `fibonacci` with `n=4`:

A call graph shows a set function frames, with lines connecting each frame to the frames of the functions it calls. At the top of the graph, `fibonacci` with `n=4` calls `fibonacci` with `n=3` and `n=2`. In turn, `fibonacci` with `n=3` calls `fibonacci` with `n=2` and `n=1`. And so on.

Count how many times `fibonacci(0)` and `fibonacci(1)` are called. This is an inefficient solution to the problem, and it gets far worse as the argument gets bigger.

A good solution is to keep track of values that have already been computed by storing them in a dictionary. A previously computed value that is stored for later use is called a hint. Here is an implementation of `fibonacci` using hints:

    previous = {0: 0, 1: 1}

    def fibonacci(n):
        if previous.has_key(n):
            return previous[n]
        else:
            new_value = fibonacci(n-1) + fibonacci(n-2)
            previous[n] = new_value
            return new_value

The dictionary named `previous` keeps track of the Fibonacci numbers we already know. We start with only two pairs: 0 maps to 1; and 1 maps to 1.

Whenever `fibonacci` is called, it checks the dictionary to determine if it contains the result. If it's there, the function can return immediately without making any more recursive calls. If not, it has to compute the new value. The new value is added to the dictionary before the function returns.

Using this version of `fibonacci`, our machines can compute `fibonacci(100)` in an eyeblink.

    >>> fibonacci(100)
    573147844013817084101L

The `L` at the end of the number indicates that it is a `long` integer.

## 12.6 Long integers

Python provides a type called `long` that can handle any size integer (limited only by the amount of memory you have on your computer).

There are three ways to create a `long` value. The first one is to compute an arithmetic expression too large to fit inside an `int`. We already saw this in the `fibonacci(100)` example above. Another way is to write an integer with a capital `L` at the end of your number:

    >>> type(1L)
    <type 'long'>

The third is to use the `long` function to convert a value to a `long`. The `long` function, just like the `int` and `float` functions, can accept any numerical type and even strings of digits:

    >>> long(1)
    1L
    >>> long(3.9)
    3L
    >>> long('57')
    57L

## 12.7 Counting letters

In Chapter 7, we wrote a function that counted the number of occurrences of a letter in a string. A more general version of this problem is to form a histogram of the letters in the string, that is, how many times each letter appears.

Such a histogram might be useful for compressing a text file. Because different letters appear with different frequencies, we can compress a file by using shorter codes for common letters and longer codes for letters that appear less frequently.

Dictionaries provide an elegant way to generate a histogram:

    >>> letter_counts = {}
    >>> for letter in "Mississippi":
    ...   letter_counts[letter] = letter_counts.get (letter, 0) + 1
    ...
    >>> letter_counts
    {'M': 1, 's': 4, 'p': 2, 'i': 4}

We start with an empty dictionary. For each letter in the string, we find the current count (possibly zero) and increment it. At the end, the dictionary contains pairs of letters and their frequencies.

It might be more appealing to display the histogram in alphabetical order. We can do that with the `items` and `sort` methods:

    >>> letter_items = letter_counts.items()
    >>> letter_items.sort()
    >>> print letter_items
    [('M', 1), ('i', 4), ('p', 2), ('s', 4)]

You have seen the `items` method before, but `sort` is the first method you have encountered that applies to lists. There are several other list methods, including `append`, `extend`, and `reverse`. Consult the Python documentation for details.

## 12.8 Glossary

dictionary:
  A collection of key-value pairs that maps from keys to values. The keys can be any immutable type, and the values can be any type.
key:
  A value that is used to look up an entry in a dictionary.
key-value pair:
  One of the items in a dictionary.
method:
  A kind of function that is called with a different syntax and invoked on an object.
invoke:
  To call a method.
hint:
  Temporary storage of a precomputed value to avoid redundant computation.
overflow:
  A numerical result that is too large to be represented in a numerical format.

## 12.9 Exercises

1. Write a program that reads in a string on the command line and returns a table of the letters of the alphabet in alphabetical order which occur in the string together with the number of times each letter occurs. Case should be ignored. A sample run of the program would look this this:

       $ python letter_counts.py "ThiS is String with Upper and lower case Letters."
       a  2
       c  1
       d  1
       e  5
       g  1
       h  2
       i  4
       l  2
       n  2
       o  1
       p  2
       r  4
       s  5
       t  5
       u  1
       w  2
       $

Table of Contents | Index