Michiel Karrenbelt
Michiel Karrenbelt

Reputation: 143

nested dictionary to object-like dict with properties

say I have two (simple toy) nested data structure like this:

d = dict(zip(list('abc'), list(range(3))))
nested_dict = {k:d.copy() for k in d}
nested_listof_dict = {k:[d.copy() for _ in range(3)] for k in d} 

Now I want to make this behave more like a 'regular' class-like object (meaning dot-indexable)

class dictobj(dict):
    def __init__(self, data: dict, name):
        data['_name'] = name
        super().__init__(data)
        for name, item in data.items():
            if isinstance(item, (list, tuple)):
                setattr(self, name, [dictobj(x, name) if isinstance(x, dict) else x for x in item])
            else:
                setattr(self, name, dictobj(item, name) if isinstance(item, dict) else item)

    def __repr__(self):
        return f"{self['_name']}"


data_dictobj = dictobj(data, 'test')  # size 1185 bytes

which works nicely for both the nested dict and nested_listof_dict

assert nested_listof_dict.a[0].b == nested_listof_dict['a'][0]['b']

but, since both attributes and dictionaries are mutable, this might happen

nested_listof_dict['a'][0]['b'] = 2
assert nested_listof_dict.a[0].b != nested_listof_dict['a'][0]['b']  # unwanted behavior

So, therefore it would be a good idea to implement the attributes as properties. I figured it would probably be a good idea to avoid using lambda functions because of closure scoping. First looking at getting the getter implemented, I focused on the nested_dict, since it's a simpler structure.

class dictobj(dict):

    def __init__(self, data: dict, name):

        def make_property(self, name, item):
            def getter(self):
                return dictobj(item, name) if isinstance(item, dict) else item
            setattr(self.__class__, name, property(getter))
            # def setter(self, value):
            #     if not isinstance(value, type(item)):
            #         raise ValueError(f'cannot change the data structure, expected '+
            #                      f'{type(item).__name__} got {type(value).__name__}')
            #     self[name] = value
            # setattr(self.__class__, name, property(getter, setter))

        data['_name'] = name
        super().__init__(data)
        for name, item in data.items():
            if isinstance(item, (list, tuple)):
                setattr(self, name, [dictobj(x, name) if isinstance(x, dict) else x for x in item])
            else:
                make_property(self, name, item)

    def __repr__(self):
        return f"{self['_name']}"

then test if the the attribute can no longer be set

d = dictobj(d, 'test')
# d.a = 1  # fails as should: "AttributeError: can't set attribute"
# d.a.a = 1  # fails as should: "AttributeError: can't set attribute"

But somehow I am still messing up, the following behavior is observed:

print(d.a)  # returns object "a" - as desired
print(d.a)  # returns 0 - second call returns the nested value

I don't know how to avoid this behavior from occurring. Apart from that, I would also like to generate a setter that enforces the data structure to be maintained. Un-out-commenting the setter I wrote above, not surprisingly, also yields unintended behavior

d.a = {1}  # ValueError: cannot change the data structure, expected dict got set - as desired
d.a.a = 2  # AttributeError: 'int' object has no attribute 'a'
d.a = 2 
assert d.a == 0 and d['a'] == 2  # again unintended

I would like to understand what I'm doing wrong, and to make this work. It should also be noted that I have not even yet considered generating properties for the nested_listof_dict, which would also be needed.

Upvotes: 1

Views: 627

Answers (1)

Michiel Karrenbelt
Michiel Karrenbelt

Reputation: 143

munch does exactly what I need

Upvotes: 1

Related Questions