When is `__dict__` re-initialized?

Question:

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.

Asked By: Ziyuan

||

Answers:

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.

Answered By: user2357112

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.

Answered By: Sraw

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__
Answered By: Marquinho Peli
Categories: questions Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.