2

Here is a simple code of attribute dict:

class AttrDict(dict):
    def __init__(self, *args, **kwargs):
        super(AttrDict, self).__init__(*args, **kwargs)
        self.__dict__ = self

More context: https://stackoverflow.com/a/14620633/1179925

I can use it like:

d = AttrDict({'a':1, 'b':2})
print(d)

I want this to be possible:

d.b = 10
print(d)

But I want this to be impossible:

d.c = 4
print(d)

Is it possible to throw an error on new key creation?

mrgloom
  • 20,061
  • 36
  • 171
  • 301
  • Why don't you create an object insted of a dict. That way new attributes can't be added but existing ones can be changed. – Chognificent Sep 19 '19 at 11:24
  • Possible duplicate of [Prevent creating new attributes outside \_\_init\_\_](https://stackoverflow.com/questions/3603502/prevent-creating-new-attributes-outside-init) – Diptangsu Goswami Sep 19 '19 at 11:25
  • Have a look here too: https://stackoverflow.com/questions/39440190/a-workaround-for-pythons-missing-frozen-dict-type/39440252 – Valentino Sep 19 '19 at 11:25
  • @Chognificent Class in python don't garantee this. – mrgloom Sep 19 '19 at 12:02

2 Answers2

4

You could check if they are already in there

class AttrDict(dict):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.__dict__ = self

    def __setattr__(self, key, value):
        if key not in [*self.keys(), '__dict__']:
            raise KeyError('No new keys allowed')
        else:
            super().__setattr__(key, value)

    def __setitem__(self, key, value):
        if key not in self:
            raise KeyError('No new keys allowed')
        else:
            super().__setitem__(key, value)

First I thought this would be a bad idea since no initial values could be added but from the builtins it states the following: dict(iterable) -> new dictionary initialized as if via: d = {} for k, v in iterable: d[k] = v

So this does allow you to change the methods without them having effect on the initialization of the Class as it creates a new one from {} instead of from its own instance.

They will be able to change __ dict __ always though..

Marc
  • 1,539
  • 8
  • 14
  • Can you elaborate on why we a checking `key` in `'__dict__'` in `__setattr__`? – mrgloom Sep 19 '19 at 13:30
  • In order for you to actually use the the dict values as attributes the self.__dict__ = self call is needed. This would fail if __dict__ is not allowed in __setattr__. It would be possible to use some sort of freeze switch as well. – Marc Sep 19 '19 at 13:33
  • What is `freeze switch` ? – mrgloom Sep 19 '19 at 13:36
  • Where the class keeps track of it being "frozen" so in the init it would set some value "frozen" from False first to True at the end, And then in the special methods you check if that value is set or not to check if keys are in the dict or not. Thus allowing you to set the dict initially but not afterwards – Marc Sep 19 '19 at 13:38
  • Looks like this method is used in this answer https://stackoverflow.com/a/58010685/1179925 – mrgloom Sep 19 '19 at 13:40
  • I've tried that here. It locks setting new keys in some cases. However, you can always use the `.update( {key: value} )` to add key-value pairs. – danz Oct 21 '22 at 22:35
  • Interesting, in the docs it states that .update() uses `for k in E: D[k] = E[k]` which implies it uses the `__setitem__` magic function, but it's not being called here. You can override the update method and raise an error offcourse. – Marc Oct 26 '22 at 08:32
1

You can override the __setattr__ special method.

class AttrDict(dict):
    def __init__(self, *args, **kwargs):
        super(AttrDict, self).__setattr__('_initializing', True)
        super(AttrDict, self).__init__(*args, **kwargs)
        self.__dict__ = self
        super(AttrDict, self).__setattr__('_initializing', False)

    def __setattr__(self, x, value):
        if x == '_initializing':
            raise KeyError("You should not edit _initalizing")
        if self._initializing or x in self.__dict__.keys():
            super(AttrDict, self).__setattr__(x, value)
        else:
            raise KeyError("No new keys allowed!")

Note that I needed to add an attribute _initializing to let __setattr__ distinguish between attributes created by __init__ and attributes created by users.
Since python does not have private attributes, users might still set _initializing to True and then add their attributes to the AttrDict instance, so I added a further check to be sure that they are not trying to edit _initializing.
It is still not 100% safe, since an user could still use super() to set _initializing to True.

Valentino
  • 7,291
  • 6
  • 18
  • 34