17

Does anyone know if there's a standard class for an infinitely nestable dictionary in Python?

I'm finding myself repeating this pattern:

d = defaultdict(lambda: defaultdict(lambda: defaultdict(int)))
d['abc']['def']['xyz'] += 1

If I want to add "another layer" (e.g. d['abc']['def']['xyz']['wrt']), I have to define another nesting of defaultdicts.

To generalize this pattern, I've written a simple class that overrides __getitem__ to automatically create the next nested dictionary.

e.g.

d = InfiniteDict(('count',0),('total',0))
d['abc']['def']['xyz'].count += 0.24
d['abc']['def']['xyz'].total += 1
d['abc']['def']['xyz']['wrt'].count += 0.143
d['abc']['def']['xyz']['wrt'].total += 1

However, does anyone know of a pre-existing implementation of this idea? I've tried Googling, but I'm not sure what this would be called.

smci
  • 32,567
  • 20
  • 113
  • 146
Cerin
  • 60,957
  • 96
  • 316
  • 522

6 Answers6

25

I think this one-liner is a nearly perfect solution:

>>> from collections import defaultdict
>>> infinite_defaultdict = lambda: defaultdict(infinite_defaultdict)
>>> d = infinite_defaultdict() 
>>> d['x']['y']['z'] = 10

by Raymond Hettinger on Twitter (https://twitter.com/raymondh/status/343823801278140417)

Quentin Pradet
  • 4,691
  • 2
  • 29
  • 41
michaelkrisper
  • 707
  • 6
  • 8
15

This lends itself naturally to a recursive definition.

>>> import collections
>>> def nested_dd():
...     return collections.defaultdict(nested_dd)
...
>>> foo = nested_dd()
>>> foo
defaultdict(<function nested_dd at 0x023F0E30>, {})
>>> foo[1][2]=3
>>> foo[1]
defaultdict(<function nested_dd at 0x023F0E30>, {2: 3})
>>> foo[1][2]
3
Katriel
  • 120,462
  • 19
  • 136
  • 170
  • 1
    Very tidy, but has the same flaw as my posted answer, no support for 'count' and 'total' zero values at the leaf nodes. – PaulMcG Nov 14 '10 at 16:15
  • 1
    @Paul -- indeed; you can't have your cake and eat it too! Having special support for `count` and `total` breaks symmetry, so it would have to be hardwired into the class somewhere. – Katriel Nov 14 '10 at 16:17
  • 1
    @aaronasterling: There is not problem with any recursion limits since the function doesn't call itself recursively. – sth Nov 14 '10 at 16:17
  • right, I missed that part and kept looking :) Sorry katriealex. – aaronasterling Nov 14 '10 at 16:19
  • pardon me, but i don't think this working because if you see the question of the OP he is doing a lot calculation using the default value of the defaultdict and not just assignment !!! – mouad Nov 14 '10 at 16:33
14

You can derive from defaultdict to get the behavior you want:

class InfiniteDict(defaultdict):
   def __init__(self):
      defaultdict.__init__(self, self.__class__)

class Counters(InfiniteDict):
   def __init__(self):
      InfiniteDict.__init__(self)                                               
      self.count = 0
      self.total = 0

   def show(self):
      print "%i out of %i" % (self.count, self.total)

Usage of this class would look like this:

>>> d = Counters()
>>> d[1][2][3].total = 5
>>> d[1][2][3].show()
0 out of 5
>>> d[5].show()
0 out of 0
sth
  • 222,467
  • 53
  • 283
  • 367
  • 1
    Thanks. This comes the closest to what I was looking for, and helped me find the *exact* solution. – Cerin Nov 14 '10 at 16:32
4

The ideal solution, inspired by sth's answer:

from collections import defaultdict

class InfiniteDict(defaultdict):
   def __init__(self, **kargs):
      defaultdict.__init__(self, lambda: self.__class__(**kargs))
      self.__dict__.update(kargs)

d = InfiniteDict(count=0, total=0)
d['abc']['def'].count += 0.25
d['abc']['def'].total += 1
print d['abc']['def'].count
print d['abc']['def'].total
d['abc']['def']['xyz'].count += 0.789
d['abc']['def']['xyz'].total += 1
print d['abc']['def']['xyz'].count
print d['abc']['def']['xyz'].total
Cerin
  • 60,957
  • 96
  • 316
  • 522
4

In case after eight years you are still thinking about how to get this with a one-liner:

from collections import defaultdict

t = defaultdict(lambda: defaultdict(t.default_factory))
Tommaso Barbugli
  • 11,781
  • 2
  • 42
  • 41
0

This is close:

class recursivedefaultdict(defaultdict):
    def __init__(self, attrFactory=int):
        self.default_factory = lambda : type(self)(attrFactory)
        self._attrFactory = attrFactory
    def __getattr__(self, attr):
        newval = self._attrFactory()
        setattr(self, attr, newval)
        return newval

d = recursivedefaultdict(float)
d['abc']['def']['xyz'].count += 0.24  
d['abc']['def']['xyz'].total += 1  

data = [
    ('A','B','Z',1),
    ('A','C','Y',2),
    ('A','C','X',3),
    ('B','A','W',4),
    ('B','B','V',5),
    ('B','B','U',6),
    ('B','D','T',7),
    ]

table = recursivedefaultdict(int)
for k1,k2,k3,v in data:
    table[k1][k2][k3] = v

It's not quite what you want, since the most deeply nested level does not have your default 0 values for 'count' or 'total'.

Edited: Ah, this works now - just needed to add a __getattr__ method, and this does what you want.

Edit 2: Now you can define other factory methods for the attributes, besides ints. But they all have to be the same type, can't have count be float and total be int.

PaulMcG
  • 62,419
  • 16
  • 94
  • 130