Reputation: 143
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