18

So I'm writing a class that extends a dictionary which right now uses a method "dictify" to transform itself into a dict. What I would like to do instead though is change it so that calling dict() on the object results in the same behavior, but I don't know which method to override. Is this not possible, or I am I missing something totally obvious? (And yes, I know the code below doesn't work but I hope it illustrates what I'm trying to do.)

from collections import defaultdict

class RecursiveDict(defaultdict):
    '''
    A recursive default dict.

    >>> a = RecursiveDict()
    >>> a[1][2][3] = 4
    >>> a.dictify()
    {1: {2: {3: 4}}}
    '''
    def __init__(self):
        super(RecursiveDict, self).__init__(RecursiveDict)

    def dictify(self):
        '''Get a standard dictionary of the items in the tree.'''
        return dict([(k, (v.dictify() if isinstance(v, dict) else v))
                     for (k, v) in self.items()])

    def __dict__(self):
        '''Get a standard dictionary of the items in the tree.'''
        print [(k, v) for (k, v) in self.items()]
        return dict([(k, (dict(v) if isinstance(v, dict) else v))
                     for (k, v) in self.items()])

EDIT: To show the problem more clearly:

>>> b = RecursiveDict()
>>> b[1][2][3] = 4
>>> b
defaultdict(<class '__main__.RecursiveDict'>, {1: defaultdict(<class '__main__.RecursiveDict'>, {2: defaultdict(<class '__main__.RecursiveDict'>, {3: 4})})})
>>> dict(b)
{1: defaultdict(<class '__main__.RecursiveDict'>, {2: defaultdict(<class '__main__.RecursiveDict'>, {3: 4})})}
>>> b.dictify()
{1: {2: {3: 4}}}

I want dict(b) to be same as b.dictify()

Ceasar
  • 22,185
  • 15
  • 64
  • 83
  • 2
    Some of the answers to this question suggest overriding `__iter__`, so that it returns key-value pairs in the same manner as `dict.iteritems`. That might be a solution that suits your needs, but it will mean that your class no longer correctly implements the same interface as `dict` itself, since `dict.__iter__` is the same method as `dict.iterkeys`. keep this in mind. – SingleNegationElimination Jul 21 '11 at 19:53
  • 1
    Are you trying to change the repr or to remove the automatic initialisation on a missing key? If you want the latter: The repr in the accepted answer is misleading, the inner dictionary is NOT a normal dictionary. – Rosh Oxymoron Jul 21 '11 at 20:55

6 Answers6

29

Nothing wrong with your approach, but this is similar to the Autovivification feature of Perl, which has been implemented in Python in this question. Props to @nosklo for this.

class RecursiveDict(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

>>> a = RecursiveDict()
>>> a[1][2][3] = 4
>>> dict(a)
{1: {2: {3: 4}}}

EDIT

As suggested by @Rosh Oxymoron, using __missing__ results in a more concise implementation. Requires Python >= 2.5

class RecursiveDict(dict):
    """Implementation of perl's autovivification feature."""
    def __missing__(self, key):
        value = self[key] = type(self)()
        return value
Community
  • 1
  • 1
Rob Cowie
  • 22,259
  • 6
  • 62
  • 56
  • So that works when RecursiveDict inherits from dict which is awesome, although the line ">>> a[1][2][3] = 4" doesn't, but not when the parent class is a defaultdict. – Ceasar Jul 21 '11 at 19:26
  • Correct. This implementation does not use defaultdict. Is that critical? If so, I misunderstood your question – Rob Cowie Jul 21 '11 at 19:44
  • 1
    The problem with this is that dict(a) is still RecursiveDict, not dict. – Evgeny Jul 21 '11 at 19:47
  • 1
    Actually, this is a much better implementation that achieves the effect I was hoping to emulate. I was locked in the wrong mindset. – Ceasar Jul 21 '11 at 19:52
  • @Evgeny. I'm not sure that's true. type(dict(a)) yields on 2.7 – Rob Cowie Jul 21 '11 at 20:04
  • Yeah, agreeing with Ceasar here. Inheriting from dict directly removes the need for a dictify/dict() in the first place. – AlexeyMK Jul 21 '11 at 20:44
  • 5
    You could shorten this to the code of your except clause if you simply define `__missing__(self, item)`. – Rosh Oxymoron Jul 21 '11 at 20:46
  • @Rob Cowie Oh, I see. I'm in 2.6 and didn't know about difference. Thanks! – Evgeny Jul 21 '11 at 21:40
2

edit: As ironchefpython pointed out in comments, this isn't actually doing what I thought it did, as in my example b[1] is still a RecursiveDict. This may still be useful, as you essentially get an object pretty similar Rob Cowie's answer, but it is built on defaultdict.


You can get the behavior you want (or something very similar) by overriding __repr__, check this out:

class RecursiveDict(defaultdict):
    def __init__(self):
        super(RecursiveDict, self).__init__(RecursiveDict)

    def __repr__(self):
        return repr(dict(self))

>>> a = RecursiveDict()
>>> a[1][2][3] = 4
>>> a             # a looks like a normal dict since repr is overridden
{1: {2: {3: 4}}}
>>> type(a)
<class '__main__.RecursiveDict'>
>>> b = dict(a)
>>> b             # dict(a) gives us a normal dictionary
{1: {2: {3: 4}}}
>>> b[5][6] = 7   # obviously this won't work anymore
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError: 5
>>> type(b)
<type 'dict'>

There may be a better way to get to a normal dictionary view of the defaultdict than dict(self) but I couldn't find one, comment if you know how.

Andrew Clark
  • 202,379
  • 35
  • 273
  • 306
  • 1
    `__repr__` has absolutely nothing to do with `dict`. You are merely changing how a `RecursiveDict` displays, and in your example, `type(b[1])` returns `` – ironchefpython Jul 21 '11 at 21:44
  • @ironchefpython - Good catch, I edited my answer to reflect that. Rob Cowie's answer has the same issue, when `dict()` is called on a nested structure using either of our answers, the inner dictionaries will still be `RecursiveDict`s. – Andrew Clark Jul 21 '11 at 22:30
2

Do you want just to print it like a dict ? use this:

from collections import defaultdict

class RecursiveDict(defaultdict):
    '''
    A recursive default dict.

    >>> a = RecursiveDict()
    >>> a[1][2][3] = 4
    >>> a.dictify()
    {1: {2: {3: 4}}}
    >>> dict(a)
    {1: {2: {3: 4}}}

    '''
    def __init__(self):
        super(RecursiveDict, self).__init__(RecursiveDict)

    def dictify(self):
        '''Get a standard dictionary of the items in the tree.'''
        return dict([(k, (v.dictify() if isinstance(v, dict) else v))
                     for (k, v) in self.items()])

    def __dict__(self):
        '''Get a standard dictionary of the items in the tree.'''
        print [(k, v) for (k, v) in self.items()]
        return dict([(k, (dict(v) if isinstance(v, dict) else v))
                     for (k, v) in self.items()])

    def __repr__(self):
        return repr(self.dictify())

Maybe you are looking for __missing__ :

class RecursiveDict(dict):
    '''
    A recursive default dict.

    >>> a = RecursiveDict()
    >>> a[1][2][3] = 4
    >>> a
    {1: {2: {3: 4}}}
    >>> dict(a)
    {1: {2: {3: 4}}}

    '''

    def __missing__(self, key):
        self[key] = self.__class__()
        return self[key]
mouad
  • 67,571
  • 18
  • 114
  • 106
2

You can't do it.

I deleted my previous answer, because I found after looking at the source code, that if you call dict(d) on a d that is a subclass of dict, it makes a fast copy of the underlying hash in C, and returns a new dict object.

Sorry.

If you really want this behavior, you'll need to create a RecursiveDict class that doesn't inherit from dict, and implement the __iter__ interface.

ironchefpython
  • 3,409
  • 1
  • 19
  • 32
1

You need to override __iter__.

def __iter__(self): 
    return iter((k, (v.dictify() if isinstance(v, dict) else v)) 
                for (k, v) in self.items())

Instead of self.items(), you should use self.iteritems() on Python 2.

Edit: OK, This seems to be your problem:

>>> class B(dict): __iter__ = lambda self: iter(((1, 2), (3, 4)))
... 
>>> b = B()
>>> dict(b)
{}
>>> class B(list): __iter__ = lambda self: iter(((1, 2), (3, 4)))
... 
>>> b = B()
>>> dict(b)
{1: 2, 3: 4}

So this method doesn't work if the object you're calling dict() on is a subclass of dict.

Edit 2: To be clear, defaultdict is a subclass of dict. dict(a_defaultdict) is still a no-op.

agf
  • 171,228
  • 44
  • 289
  • 238
  • So this still doesn't work exactly right: >>> a = RecursiveDict() >>> a[1][2][3] = 4 >>> for b in a: print b (1, {2: {3: 4}}) >>> dict(a) {1: defaultdict(, {2: defaultdict(, {3: 4})})} – Ceasar Jul 21 '11 at 19:19
  • It turns out this just won't work if your class is subclassing `dict` because `dict(already_a_dict)` doesn't do anything. – agf Jul 21 '11 at 19:35
  • Er, I'm subclassing defaultdict, so it's a little different. – Ceasar Jul 21 '11 at 19:39
  • No, it's not. `dict()` will still do nothing because `defaultdict` is a subclass of `dict`. – agf Jul 21 '11 at 19:40
0

Once you have your dictify function working just do

dict = dictify

Update: Here is a short way to have this recursive dict:

>>> def RecursiveDict():
...   return defaultdict(RecursiveDict)

Then you can:

d[1][2][3] = 5
d[1][2][4] = 6
>>> d
defaultdict(<function ReturnsRecursiveDict at 0x7f3ba453a5f0>, {1: defaultdict(<function ReturnsRecursiveDict at 0x7f3ba453a5f0>, {2: defaultdict(<function ReturnsRecursiveDict at 0x7f3ba453a5f0>, {3: 5, 4: 6})})})

I don't see a neat way to implement dictify.

Evgeny
  • 3,064
  • 2
  • 18
  • 19