Sunday, May 26, 2019

Nested attribute lookup in dicts

As part of a larger project, I wanted to explore the use of Python object  attribute access syntax to access items of a dict.

A base.

I looked around and found this article that gave me a base I could riff off. I want the attributes accessed to appear in the dictionary. I want nested attribut access to "work".

A new implementation.

I came up with the following code. any attributes starting with an underscore are excused from jiggery-pokery as Spyder/Ipython sets a few.

class AttrInDict(dict):
    "Move none-hidden attribute access to dict item"
 
    def __init__(self, *args, **kwargs):
        self._extra_attr = set()
        super().__init__(*args, **kwargs)
 
    def __getattr__(self, item):
        if item[0] != '_':
            if (not super().__contains__(item)  # Its new
                or (item in self._extra_attr    # It's an attr, now None
                    and super().__getitem__(item) is None)):
                super().__setitem__(item, AttrInDict())
            return super().__getitem__(item)
        else:
            return super().__getattr__(item)
 
    def __setattr__(self, item, val):
        if item[0] != '_':
            super().__setitem__(item, val)
            self._extra_attr.add(item)
        else:
            super().__setattr__(item, val)
 
    def __dir__(self):
        "To get tooltips working"
        supr = set(super().__dir__())
        return list(supr | self._extra_attr)


Class in action.

Python 3.7.1 | packaged by conda-forge | (default, Mar 13 2019, 13:32:59) [MSC v.1900 64 bit (AMD64)]
Type "copyright", "credits" or "license" for more information.

IPython 7.1.1 -- An enhanced Interactive Python.

Restarting kernel...



In [1]: runfile('dictindict.py', wdir='pp_reg')

In [2]: # Start like a dict

In [3]: d = AttrInDict(x=3)

In [4]: d
Out[4]: {'x': 3}

In [5]: # Access like an attribute

In [6]: d.x
Out[6]: 3

In [7]: # Access an unknown attribute creates a sub-"dict"

In [8]: d.foo
Out[8]: {}

In [9]: d
Out[9]: {'x': 3, 'foo': {}}

In [10]: d.foo = 123

In [11]: d.foo
Out[11]: 123

In [12]: d
Out[12]: {'x': 3, 'foo': 123}

In [13]: # Access an unknown, unknown attribute creates sub, sub "dicts"

In [14]: d.tick.tock
Out[14]: {}

In [15]: d
Out[15]: {'x': 3, 'foo': 123, 'tick': {'tock': {}}}

In [16]: # Dict hierarchy preserved

In [17]: d.tick.tack = 22

In [18]: d
Out[18]: {'x': 3, 'foo': 123, 'tick': {'tock': {}, 'tack': 22}}

In [19]: d.tick.teck = 33

In [20]: d
Out[20]: {'x': 3, 'foo': 123, 'tick': {'tock': {}, 'tack': 22, 'teck': 33}}

In [21]: d.tick.tock.tuck = 'Boo'

In [22]: d
Out[22]: {'x': 3, 'foo': 123, 'tick': {'tock': {'tuck': 'Boo'}, 'tack': 22, 'teck': 33}}

In [23]: # Can tack on hierarchy to previous attribute with None value

In [24]: d.foo = None

In [25]: d
Out[25]:
{'x': 3,
'foo': None,
'tick': {'tock': {'tuck': 'Boo'}, 'tack': 22, 'teck': 33}}

In [26]: d.foo.bar = 42

In [27]: d
Out[27]:
{'x': 3,
'foo': {'bar': 42},
'tick': {'tock': {'tuck': 'Boo'}, 'tack': 22, 'teck': 33}}

In [28]: # Still like a dict

In [29]: d.keys()
Out[29]: dict_keys(['x', 'foo', 'tick'])

In [30]: d.values()
Out[30]: dict_values([3, {'bar': 42}, {'tock': {'tuck': 'Boo'}, 'tack': 22, 'teck': 33}])

In [31]:

END.