83

A lot of times in Perl, I'll do something like this:

$myhash{foo}{bar}{baz} = 1

How would I translate this to Python? So far I have:

if not 'foo' in myhash:
    myhash['foo'] = {}
if not 'bar' in myhash['foo']:
    myhash['foo']['bar'] = {}
myhash['foo']['bar']['baz'] = 1

Is there a better way?

nosklo
  • 193,422
  • 54
  • 273
  • 281
mike
  • 40,704
  • 43
  • 97
  • 111

4 Answers4

115

If the amount of nesting you need is fixed, collections.defaultdict is wonderful.

e.g. nesting two deep:

myhash = collections.defaultdict(dict)
myhash[1][2] = 3
myhash[1][3] = 13
myhash[2][4] = 9

If you want to go another level of nesting, you'll need to do something like:

myhash = collections.defaultdict(lambda : collections.defaultdict(dict))
myhash[1][2][3] = 4
myhash[1][3][3] = 5
myhash[1][2]['test'] = 6

edit: MizardX points out that we can get full genericity with a simple function:

import collections
def makehash():
    return collections.defaultdict(makehash)

Now we can do:

myhash = makehash()
myhash[1][2] = 4
myhash[1][3] = 8
myhash[2][5][8] = 17
# etc
Mark Lakata
  • 18,024
  • 5
  • 88
  • 112
John Fouhy
  • 37,154
  • 18
  • 59
  • 73
  • 6
    or def makehash(): return collections.defaultdict(makehash); myhash = makehash() – Markus Jarderot Mar 16 '09 at 21:56
  • 1
    I've got no problems with "traditional" recursive functions, but there's something about that that I find unintuitive. Odd. Anyway, thanks! – John Fouhy Mar 16 '09 at 23:06
  • 1
    Thanks for this. That lambda: defaultdict() is what I needed. – wheaties Feb 08 '10 at 15:48
  • Unfortunately that recursive makehash approach does not handle the following a = makehash(); a['foo'] += 1; as I understand it's because the default_factory is not specified here. Do you know how to overcome this issue? – Anton Daneyko May 06 '10 at 08:02
  • This just saved my bacon on a data conversion project that was overdue before I started it. I needed to do some time coalescing of log messages based on sec,msec tags; I really wanted to use hash-hash-list. tagtable = collections.defaultdict(lambda : collections.defaultdict(list)) really hit the spot. – Wexxor Feb 14 '13 at 06:58
  • makehash() is awesome. But is there a pythonic way to accomplish the same thing with a default value as well? Difficult, because the default value *is* a defaultdict()? – PonyEars Jul 17 '14 at 22:40
  • 1
    very handy it is when initializing, but it seems there's no easy way to work with this dict since there's lambda function involved, e.g. `print myhash` will get a bunch of ` at ...>` sort of thing. – timfeirg Oct 10 '14 at 08:52
  • @timfeirg you can use json lib to overcome this: `import json; print json.dumps(myhash)`. – diegopso Dec 18 '19 at 15:47
106
class AutoVivification(dict):
    """Implementation of perl's autovivification feature."""
    def __getitem__(self, item):
        try:
            return dict.__getitem__(self, item)
        except KeyError:
            value = self[item] = type(self)()
            return value

Testing:

a = AutoVivification()

a[1][2][3] = 4
a[1][3][3] = 5
a[1][2]['test'] = 6

print a

Output:

{1: {2: {'test': 6, 3: 4}, 3: {3: 5}}}
nosklo
  • 193,422
  • 54
  • 273
  • 281
  • 4
    Is it possible to extend it so it supports the following behavior: a[1][2][3] += some_value. So if the key did not exist in advance, then the a[1][2][3] would be initialized with the default value of the type(some_value)? – Anton Daneyko May 06 '10 at 08:08
  • 5
    This function has the side effect that any attempts to get a non-existent key also creates the key. Typically you would only want to auto create a key if you were at the same time setting a key or subkey. – Dave Rawks Feb 21 '12 at 22:29
  • Is there also a way to make the assignment variable? So that given `var = [1,2,3]`, I could do like `a[var] = 1`, which would expand to `a[1][2][3] = 1`? – PascalVKooten Oct 13 '13 at 15:06
  • 1
    `var = [1,2]` would then allow `a[var] = 1` to be `a[1][2] = 1` – PascalVKooten Oct 13 '13 at 15:07
  • 1
    @Dualinity that already works if you use normal dicts and tuples: `d = {}; k = (1, 2, 3); d[k] = 1` then you can use `d[1,2,3]`. If you want to use `d[1][2][3]` (why?) you'd have to modify the recipe above, ask another question. remember tat flat is better than nested. – nosklo Oct 17 '13 at 11:13
  • Because I want to have a structure with – PascalVKooten Oct 17 '13 at 12:40
  • Another solution would be... `from collections import defaultdict` followed by `AutoVivification = lambda: defaultdict(AutoVivification)` This one seems more simple. Subclassing dict though has the side effect of the dictionary being easier to read when printed though. Source: http://en.wikipedia.org/wiki/Autovivification#Python – eric.frederich Jan 20 '15 at 17:05
  • @AntonDaneyko to auto initialize a[1][2][3] += some_value you can overwrite the method `def __iadd__(self, some_value): return some_value` – Daniel Pérez Rada Aug 09 '16 at 12:30
14

Is there a reason it needs to be a dict of dicts? If there's no compelling reason for that particular structure, you could simply index the dict with a tuple:

mydict = {('foo', 'bar', 'baz'):1} # Initializes dict with a key/value pair
mydict[('foo', 'bar', 'baz')]      # Returns 1

mydict[('foo', 'unbar')] = 2       # Sets a value for a new key

The parentheses are required if you initialize the dict with a tuple key, but you can omit them when setting/getting values using []:

mydict = {}                        # Initialized the dict
mydict['foo', 'bar', 'baz'] = 1    # Sets a value
mydict['foo', 'bar', 'baz']        # Returns 1
zweiterlinde
  • 13,359
  • 2
  • 25
  • 31
  • Can you be clear on when you can omit the parentheses? Is it because the comma is the tuple operator, and the parentheses only needed if we have ambiguous grouping? – Kiv Mar 16 '09 at 20:05
  • This may actually be faster than nested dictionaries because there is one lookup instead of three. – ChaimG Nov 21 '17 at 22:05
  • Yes for initializing well defined structures which are then written to json? A very common case/practice... – rubmz Mar 01 '21 at 06:00
2

I guess the literal translation would be:

 mydict = {'foo' : { 'bar' : { 'baz':1}}}

Calling:

 >>> mydict['foo']['bar']['baz']

gives you 1.

That looks a little gross to me, though.

(I'm no perl guy, though, so I'm guessing at what your perl does)

Dana
  • 28,425
  • 17
  • 58
  • 72