6

I have been reading about overriding getattr and setattr and I can't figure out if I need the overriding assignments if I use self.__dict__ = self in the constructor.

Once I make an instance of the class

a = OPT(foo='bar')

a.foo and a['foo'] work with and without the __getattr__ and __setattr__ declaration.

Can someone explain if I need both. If I do, why? Thanks!

class OPT(dict):
    __getattr__ = dict.__getitem__
    __setattr__ = dict.__setitem__

    def __init__(self, *args, **kwargs):
        super(OPT, self).__init__(*args, **kwargs)
        self.__dict__ = self
Ptrkcon
  • 1,455
  • 3
  • 16
  • 32
  • 1
    Setting `self.__dict__ = self` is probably not a good idea. What are you trying to accomplish with this code? – BrenBarn Mar 04 '14 at 03:17
  • I am accessing the class as an obj or like a dict. so I can do a['bar'] or a.bar. Like [this post](http://stackoverflow.com/questions/2641484/class-dict-self-init-args) – Ptrkcon Mar 04 '14 at 03:25
  • 4
    The problem is if someone does `a['keys'] = 2`, they will stomp on the `keys()` method of the dict, and so on. – BrenBarn Mar 04 '14 at 03:28
  • ha, I did not think of that. I've seen this implementation before. There are a lot of posts about it but I don't remember that issue coming up. – Ptrkcon Mar 04 '14 at 03:31
  • @BrenBarn's comment is really the answer here. – Henry Keiter Mar 04 '14 at 04:08
  • Be careful! - __ getattr __ = dict.__ getitem __ - will raise "KeyError" instead of "AttributeError" required by API docs: docs.python.org/3.9/reference/datamodel.html#object.__getattr__ Instead use this: def __ getattr __: if key in self: return self[key] else: raise AttributeError(key) – Marquinho Peli Oct 27 '22 at 14:27

3 Answers3

2

You override getattr and setattr when you want to do something extra when the class user gets or sets an attribute. For example:

1) you might avoid raising an exception when user manipulates an invalid attribute, so you just return None for an unknown attribute.

2) attribute manipulations are actually forwarded/delegated, so the valid attributes are not known in advance e.g. a class that represents a database row and the user manipulates columns as attributes. I need to run-time check if the given attribute name matches column name, and perhaps I'd like to forgive upper-case/lower-case differences etc.

Another thing, containment is sometimes preferred to subclassing. Instead of inheriting from a dict, you could create a class that contains a dict.

epx
  • 1,066
  • 9
  • 17
  • If I don't plan on doing anything like that I wouldn't need to override getattr and setattr? I could always pass in a default value for .get() in your first example. I don't know what an example of your 2nd point would be. – Ptrkcon Mar 04 '14 at 03:37
  • Edited the second example for more clarity. – epx Mar 04 '14 at 04:03
0

Try this:

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__
Marquinho Peli
  • 4,795
  • 4
  • 24
  • 22
  • 1
    In `__init__` you should use `d: dict=None` as the default argument value instead of `d: dict={}`. Default argument values are evaluated only once (at module load time) and are shared by all the calls to the function (here `__init__`), which may result in "extremely surprising behavior" (see Brett Slatkin's *Effective Python, 2nd ed*., "Item 24: Use `None` and Docstrings to Specify Dynamic Default Arguments") – KiriSakow Feb 09 '23 at 16:29
  • You should make sure that `d` is not `None` before iterating over `d.items()` – KiriSakow Feb 09 '23 at 16:34
  • @KiriSakow, the issue you point out doesn't happen with default values as objects. I tried this code with no issues in console: y = DotDict({"d": "ha!"}) y.c = "ok" print(y) x = DotDict() x.c = "ok" print(x) #{'d': 'ha!', 'c': 'ok'} #{'c': 'ok'} – Marquinho Peli Feb 10 '23 at 17:44
  • It normally does happen — in both functions and constructors. Unless you want to memoize, do not use empty literals as default values, use `None`. See Brett Slatkin's book for an example, or this answer: https://stackoverflow.com/questions/41692179/init-method-gets-a-value-while-none-is-passed/41692265#41692265 – KiriSakow Feb 11 '23 at 06:50
0

Here's a more elaborate fork of the Marquinho Peli's answer. Changes: support for dictionaries with nested lists;

class DotDict(dict):
    """A dictionary that's recursively navigable with dots, not brackets."""

    def __init__(self, data: dict = None):
        super().__init__()
        if data is None or not isinstance(data, dict):
            raise AttributeError(f"{type(self).__name__} must be instantiated with a dictionary, not a {type(data).__name__}.")
        for key, value in data.items():
            if isinstance(value, list):
                self[key] = [DotDict(item) for item in value]
            elif isinstance(value, dict):
                self[key] = DotDict(value)
            else:
                self[key] = value

    __setattr__ = dict.__setitem__
    __delattr__ = dict.__delitem__

    def __getattr__(self, key):
        try:
            return self[key]
        except KeyError:
            raise AttributeError(f"attribute .{key} not found")

Usage examples:

d = {'key1': 'value1',
     'key2': 'value2',
     'key3': {'key3a': 'value3a'},
     'key4': {'key4a': [{'key4aa': 'value4aa',
                         'key4ab': 'value4ab',
                         'key4ac': 'value4ac'}],
              'key4b': 'value4b'}}

dd = DotDict(d)
print(dd.key4.key4a[0].key4aa)  # value4aa
dd.key4.key4a[0].key4aa = 'newval'
print(dd.key4.key4a[0].key4aa)  # newval

print(dd.key4.key4a[0].key4aaa) # AttributeError: attribute .key4aaa not found

DotDict({}) # OK
DotDict()   # AttributeError: DotDict must be instantiated with dictionary, not a NoneType.
KiriSakow
  • 957
  • 1
  • 12
  • 22