Reputation: 4568
I subclass dict
so that the attributes are identical to the keys:
class DictWithAttr(dict):
def __init__(self, *args, **kwargs):
self.__dict__ = self
super(DictWithAttr, self).__init__(*args, **kwargs)
print(id(self), id(self.__dict__))
def copy(self):
return DictWithAttr(self.__dict__)
def __repr__(self):
return repr({k:v for k, v in self.items() if k != '__dict__'})
and it works as expected:
d = DictWithAttr(x=1, y=2) # 139917201238328 139917201238328
d.y = 3
d.z = 4
d['w'] = 5
print(d) # {'x': 1, 'y': 3, 'z': 4, 'w': 5}
print(d.__dict__) # {'x': 1, 'y': 3, 'z': 4, 'w': 5}
print(d.z, d.w) # 4 5
But if I re-write __setattr__
as
...
def __setattr__(self, key, value):
self[key] = value
...
then __dict__
will be re-created in initialization and the attributes will turn inaccessible:
d = DictWithAttr(x=1, y=2) # 140107290540344 140107290536264
d.y = 3
d.z = 4
d['w'] = 5
print(d) # {'x': 1, 'y': 3, 'z': 4, 'w': 5}
print(d.__dict__) # {}
print(d.z, d.w) # AttributeError: 'DictWithAttr' object has no attribute 'z'
Adding a paired __getattr__
as below will get around the AttributeError
...
def __getattr__(self, key):
return self[key]
...
but still __dict__
is cleared:
d = DictWithAttr(x=1, y=2) # 139776897374520 139776897370944
d.y = 3
d.z = 4
d['w'] = 5
print(d) # {'x': 1, 'y': 3, 'z': 4, 'w': 5}
print(d.__dict__) # {}
print(d.z, d.w) # 4 5
Thanks for any explanations.
Upvotes: 2
Views: 147
Reputation: 5139
Try this! Simple, addresses nested dicts and correct AttributeError, although being very small:
class DotDict(dict):
def __init__(self, d: dict = {}):
super().__init__()
for key, value in d.items():
self[key] = DotDict(value) if type(value) is dict else value
def __getattr__(self, key):
if key in self:
return self[key]
raise AttributeError(key) #Set proper exception, not KeyError
__setattr__ = dict.__setitem__
__delattr__ = dict.__delitem__
Upvotes: 0
Reputation: 20264
To achieve what you want, you should overwrite __getattr__
, __setattr__
and __delattr__
.
class DictWithAttr(dict):
def __getattr__(self, name):
return self[name]
__setattr__ = dict.__setitem__
def __delattr__(self, name):
del self[name]
def __dir__(self):
return dir({}) + list(self.keys())
The reason of your problem has been pointed out by user2357112.
Upvotes: 0
Reputation: 281958
There's no reinitialization. Your problem is that self.__dict__ = self
hits your __setattr__
override. It's not actually changing the dict used for attribute lookups. It's setting an entry for the '__dict__'
key on self
and leaving the attribute dict untouched.
If you wanted to keep your (pointless) __setattr__
override, you could bypass it in __init__
:
object.__setattr__(self, '__dict__', self)
but it'd be easier to just take out your __setattr__
override. While you're at it, take out that __repr__
, too - once you fix your code, the only reason there would be a '__dict__'
key is if a user sets it themselves, and if they do that, you should show it.
Upvotes: 3