48

Anyone have some neat examples of dictionaries with some interesting keys (besides the canonical string or integer), and how you used these in your program?

I understand all we need for a key is something hashable, meaning it must be immutable and comparable (has an __eq__() or __cmp__() method).

A related question is: how can I quickly and slickly define a new hashable?

Pete
  • 16,534
  • 9
  • 40
  • 54
  • 3
    `hashable` == you can call it's `__hash__` method and get an int. Nothing more. Comparision functions would be needed if dicts/sets/etc were e.g. binary trees. –  Dec 03 '10 at 17:55
  • I seem to have been misled by this definition: http://docs.python.org/glossary.html#term-hashable – Pete Dec 03 '10 at 18:10
  • 2
    @delnan: hashable in the context of Python also means comparable -- look at the link Pete posted. But it does not include immutable. – Sven Marnach Dec 03 '10 at 18:27
  • I stand correct. And now that you mention it, it's indeed necessary (since dicts use comparision for for collision detection). –  Dec 03 '10 at 18:40

5 Answers5

40

Let's go for something a bit more esoteric. Suppose you wanted to execute a list of functions and store the result of each. For each function that raised an exception, you want to record the exception, and you also want to keep a count of how many times each kind of exception is raised. Functions and exceptions can be used as dict keys, so this is easy:

funclist = [foo, bar, baz, quux]

results    = {}
badfuncs   = {}
errorcount = {}

for f in funclist:
    try:
        results[f] = f()
    except Exception as e:
        badfuncs[f]   = e
        errorcount[type(e)] = errorcount[type(e)] + 1 if type(e) in errorcount else 1

Now you can do if foo in badfuncs to test whether that function raised an exception (or if foo in results to see if it ran properly), if ValueError in errorcount to see if any function raised ValueError, and so on.

kindall
  • 178,883
  • 35
  • 278
  • 309
  • 1
    Neat, great example which seems like a great way to organize analysis results in memory. fix typo errorcount[t]-->errorcount[f]. Counting doesn't seem to work, not sure why it shouldn't when I iterate over the functions again without reinitializing the dict. – Pete Dec 03 '10 at 19:10
  • 1
    Actually, that was supposed to be `type(e)` rather than `t` as this dictionary should be counting the number of times each exception type was raised. I initially had a line in there that said `t = type(e)` which is where the `t` came from. Anyway, fixed. :-) – kindall Dec 03 '10 at 20:20
18

You can use a tuple as a key, for example if you want to create a multi-column index. Here's a simple example:

>>> index = {("John", "Smith", "1972/01/01"): 123, ("Bob", "Smith", "1972/01/02"): 124}
>>> index
{('Bob', 'Smith', '1972/01/02'): 124, ('John', 'Smith', '1972/01/01'): 123}
>>> index.keys()
[('Bob', 'Smith', '1972/01/02'), ('John', 'Smith', '1972/01/01')]
>>> index['John', 'Smith', '1972/01/01']
123

For an example of how to use a dict as a key (a hashable dict) see this answer: Python hashable dicts

Community
  • 1
  • 1
patrickmdnet
  • 3,332
  • 1
  • 29
  • 34
  • 2
    Note that you can omit the tuple parens, thus having a big code readability improvement (`index['John', 'Smith', '1972/01/01']`) – Gabi Purcaru Dec 03 '10 at 17:57
16

You left out the probably most important method for an object to be hashable: __hash__().

The shortest implementation of your own hashable type is this:

class A(object):
    pass

Now you can use instances of A as dictionary keys:

d = {}
a = A()
b = A()
d[a] = 7
d[b] = 8

This is because user-defined classes are hashable by default, and their hash value is their id -- so they will only compare equal if they are the same object.

Note that instances of A are by no means immutable, and they can be used as dictionary keys nevertheless. The statement that dictionary keys must be immutable only holds for the built-in types.

Sven Marnach
  • 574,206
  • 118
  • 941
  • 841
  • Being mutable and hashable at the same time is *allowed*, but it's usually insane. –  Dec 03 '10 at 17:52
  • 4
    Many kinds of Python objects are technically mutable, but are still hashable. If it were "insane", why would user-created objects be that way by default? – kindall Dec 03 '10 at 18:00
  • 2
    @kindall: I don't quite understand why `object.__hash__` behaves like this (as objects using it don't work as expected as dictionary keys). But see http://wiki.python.org/moin/DictionaryKeys for why it's one giant headache. –  Dec 03 '10 at 18:38
  • Did not know user-defined classes are hashable by default... thanks for your answer – Pete Dec 04 '10 at 19:12
6

Note that I've never really used this, but I've always thought using tuples as keys could let you do some interesting things. I would find it a convenient way to map grid coordinates, for example. You can think of this like a grid on a video game (maybe some kind of tactics game like Fire Emblem):

>>> Terrain = { (1,3):"Forest", (1,5):"Water", (3,4):"Land" }
>>> print Terrain
{(1, 5): 'Water', (1, 3): 'Forest', (3, 4): 'Land'}
>>> print Terrain[(1,3)]
Forest
>>> print Terrain[(1,5)]
Water
>>> x = 3
>>> y = 4
>>> print Terrain[(x,y)]
Land

Something like that.

Edit: As Mark Rushakof pointed out in the comments, I'm basically intending this to be a sparse array.

eldarerathis
  • 35,455
  • 10
  • 90
  • 93
  • Yes - tuples as keys are great. I have used them to store results of data sorted into bins. – dtlussier Dec 03 '10 at 17:49
  • I think everyone in one's right mind would just use a two-dimensional array to store the terrain types of the map :) – Sven Marnach Dec 03 '10 at 17:49
  • 1
    @Sven Marnach: I actually kind of like the tuples since it's a little more straightforward to skip over spaces (i.e. leave them blank). – eldarerathis Dec 03 '10 at 17:54
  • 2
    @eldarerathis and @Sven: Yes, a tuple-dictionary like this is very well-suited for [sparse arrays](http://en.wikipedia.org/wiki/Sparse_array). – Mark Rushakoff Dec 03 '10 at 17:58
  • @Mark Rushakof: Yes, precisely! I knew there was a special name for that and couldn't for the life of me remember it. Thanks! – eldarerathis Dec 03 '10 at 18:01
  • @Mark, @eldarerathis: Doing a PhD in numerical mathematics, I am more than familiar with the concept of two-dimensional sparse arrays and I know its a useful concept. My point is that the map in Fire Emblem is *not* sparse, and using this data structure for a dense array is not a good idea. No offense intended :) – Sven Marnach Dec 03 '10 at 18:51
  • 1
    @Sven Marnach: None taken. I don't think I see why this *couldn't* be a sparse array, though. Taking Fire Emblem as a specific example, if we consider a map where maybe 90% or so of the tiles have no def/avoid modifiers (not unreasonable in FE), then it seems intuitive to me to use a sparse array. The default tile type (maybe "Land" is actually best for this) could be used for any tiles that are not defined in the dictionary, I would think, and you would save some memory. Or I'm completely missing the point here, maybe. – eldarerathis Dec 03 '10 at 19:45
  • Indeed, you can even use collections.defaultdict to return the default value if an undefined key is used. – Rob Nov 20 '13 at 14:45
2

I have no idea why you'd want to do it (and preferably, please don't do it)... but alongside strings and integers, you can also use both simultaneously. That is, as a beginner, I found it both powerful and surprising that:

foo = { 1:'this', 2:'that', 'more':'other', 'less':'etc' }

is an entirely valid dictionary, which provides access to foo[2] as easily as it does to foo['more'].

omatai
  • 3,448
  • 5
  • 47
  • 74
  • This comes up when using Django in my experience. Querysets often come back with integers (e.g. ID values) for keys and they look like this when cast to dicts for JSON representation. I agree it should be avoided if possible. – medley56 Feb 22 '19 at 19:25