Switchable dot access to Python dicts?

2.9k views Asked by At

I haven't seen a toggleable version of dictionary dot-access yet.

My first-pass attempt here doesn't work:

class DottableDict (dict):

    def allowDotting (self, state=True):
        if state:
            self.__setattr__ = dict.__setitem__
            self.__getattr__ = dict.__getitem__
        else:
            del self.__setattr__
            del self.__getattr__

>>> import dot
>>> d = dot.DottableDict()
>>> d.allowDotting()
>>> d.foo = 'bar'
>>> d
{}
>>> d.foo
'bar'
>>> d.__dict__
{'__setattr__': <slot wrapper '__setitem__' of 'dict' objects>, 'foo': 'bar',
'__getattr__': <method '__getitem__' of 'dict' objects>}
>>> d.allowDotting(False)
>>> d.__dict__
{'foo': 'bar'}

I think the signatures don't match up between setattr and setitem.

My second pass also seems like it should work, but fails in the same ways:

class DottableDict (dict):

    def dotGet (self, attr):
        return dict.__getitem__(self, attr)

    def dotSet (self, attr, value):
        return dict.__setitem__(self, attr, value)

    def allowDotting (self, state=True):
        if state:
            self.__getattr__ = self.dotGet
            self.__setattr__ = self.dotSet
        else:
            del self.__setattr__
            del self.__getattr__
1

There are 1 answers

4
unutbu On BEST ANSWER

If you set self.__dict__ = self, then the dict will automatically become "dottable". You can turn off the "dot-ability" by setting self.__dict__ = {}. The key-value pairs will still be accessible through indexing, however. This idea comes mainly from katrielalex's bio page:

class DottableDict(dict):
    def __init__(self, *args, **kwargs):
        dict.__init__(self, *args, **kwargs)
        self.__dict__ = self
    def allowDotting(self, state=True):
        if state:
            self.__dict__ = self
        else:
            self.__dict__ = dict()

d = DottableDict()
d.allowDotting()
d.foo = 'bar'

print(d['foo'])
# bar
print(d.foo)
# bar

d.allowDotting(state=False)
print(d['foo'])
# bar
print(d.foo)
# AttributeError: 'DottableDict' object has no attribute 'foo'         

Remember the Zen of Python, however:

There should be one-- and preferably only one --obvious way to do it.

By restricting yourself to the standard syntax for dict access, you improve readability/maintainability for yourself and others.


How it works:

When you type d.foo, Python looks for 'foo' in a number of places, one of which is in d.__dict__. (It also looks in d.__class__.__dict__, and all the __dict__s of all the bases listed in d.__class__.mro()... For the full details of attribute lookup, see this excellent article by Shalabh Chaturvedi).

Anyway, the important point for us is that all the key-value pairs in d.__dict__ can be accessed with dot notation.

That fact means we can get dot-access to the key-value pairs in d by setting d.__dict__ to d itself! d, after all, is a dict, and d.__dict__ expects a dict-like object. Notice this is also memory-efficient. We are not copying any key-value pairs, we're simplying directing d.__dict__ to an already existent dict.

Furthermore, by assigning d.__dict__ to dict(), we effectively turn off dot-access to the key-value pairs in d. (This does not completely disable dot-access -- key-value pairs in d.__class_.__dict__ for instance, can still be accessed through dot notation. Thank goodness that's true or you wouldn't be able to call the allowDotting method again!)

Now you might be wondering if this deletes all the key-value pairs in d itself. The answer is no.

The key-value pairs are not stored in the __dict__ attribute. In fact, a normal dict does not have a __dict__ attribute. So setting d.__dict__ = {} simply resets the dict to a neutral condition. We could have used

del self.__dict__

insteaad of

self.__dict__ = dict()

too. However, since the DottableDict is given a __dict__ attribute in __init__, it seems cleaner to me to allow instances of DottableDict to always have a __dict__ attribute.


In the comments you note:

Steps: turn off access, set d.foo to 'bar', turn on access, d.foo is gone from everywhere.

To preserve attributes such as d.foo which were set while allowDotting has been turned off, you'll need to store the alternate dict to which self.__dict__ has been set.

class DottableDict(dict):
    def __init__(self, *args, **kwargs):
        dict.__init__(self, *args, **kwargs)
        self['_attributes'] = dict()
        self.allowDotting()
    def allowDotting(self, state=True):
        if state:
            self.update(self['_attributes'])
            self.__dict__ = self
        else:
            self.__dict__ = self['_attributes']

d = DottableDict()
d.allowDotting(state=False)
d.foo = 'bar'
d.allowDotting(state=True)
print(d.foo)
# bar
d.allowDotting(state=False)
print(d.foo)
# bar
d.allowDotting(state=True)
print(d.foo)
# bar

By conventional, attributes that start with a single underscore are understood to be private, implementation details. I'm extending the convention here by introducing a private key, '_attribute' into the dict.