Ziyuan
Ziyuan

Reputation: 4568

When is `__dict__` re-initialized?

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

Answers (3)

Marquinho Peli
Marquinho Peli

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

Sraw
Sraw

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

user2357112
user2357112

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

Related Questions