4

Sometimes it's nice to be able to use a dictionary like an object (so that you don't have to always write mydict['blah']. Instead, you can write mydict.blah

In Python 3, there are new rules about overriding/wrapping a python dictionary. In Python2, it was enough to wrap the __getattr__ and __setattr__ methods. When you wrap these things, you get some nice capabilities to add some special handling when a certain attribute was added (e.g. you could clean/filter data in/out of a dict).

A situation where this is helpful is in a Flask template (an HTML template). Using a __getattr__ filter, you can format data as it leaves the dict. This way, in the template (where Python expressions can look a little complex) you can keep things simple in there by just writing mymodel.blah and knowing that the text coming out of blah is already the way you want it.

In Python3 wrapping a dict is a little messy. I"m not sure how to make it work now. Here are two rough implementations that are not working well:

# messed up Python3 wrapped dictionary (sets work, but gets do not)
class AttrDict(dict):
    def __init__(self, *args, **kwargs):
        super(AttrDict, self).__init__(*args, **kwargs)
        self.__dict__ = self
        self.clean_strings()
    def clean_strings(self):
        for key, value in self.items():
            self[key] = string_empty_to_none(value)
    def __getattr__(self, name):
        #this never gets called in Python 3
        return self[name]

Here's another one:

# sets don't work - Always throws: TypeError: 'AttrDict' object does not support item assignment
class AttrDict():
    def __init__(self, *args, **kwargs):
        self.data = dict()
        self.data.__init__(*args, **kwargs)
        self.clean_strings()
    def clean_strings(self):
        for key, value in self.data.items():
            self.data[key] = string_empty_to_none(value)
    def __getattr__(self, attr):
        if attr in self.data:
            return self.data[attr]
        else:
            raise AttributeError("--%r object has no attribute %r" % (type(self).__name__, attr)) 
    def __setattr__(self, name, value):
        if name == 'data':
            super(AttrDict, self).__setattr__(name, value)
        else:
            self.data[name] = value

Here's what my little utility method looks like:

def string_empty_to_none(s):
    if type(s) == str:
            return None if (s.strip() == '') else s

    return s

I know that in Python3, you are supposed to use __getattribute__ instead of __getattr__ But that always seems to end up in an infinite loop when I do this.

NOTE: that the final syntax that I'm looking for is like this:

>>> x = AttrDict({'some': 'value'})
>>> x['blah'] = 'hello world'
>>> print(x.blah)
hello world
>>> print(x.some)
value
101010
  • 14,866
  • 30
  • 95
  • 172
  • 1
    A Python 2 implementation should work fine without modification on Python 3. There are no new rules about wrapping dicts. Your messed-up implementations fail on Python 3, but they also fail on Python 2. – user2357112 Jun 13 '18 at 16:35

3 Answers3

2

You can achieve this by overwriting using __getitem__ within __getattr__.

class AttrDict(dict):

    def __getattr__(self, item):
        return super().__getitem__(item)

    def __setattr__(self, item, value):
        return super().__setitem__(item, value)


x = AttrDict({'some': 'value'})
x['blah'] = 'hello world'
print(x.blah)  # hello world
print(x.some)  # value

# you can also assign value this way
x.foo = 'bar'
print(x['foo'])   # bar
1

Actually there is no need to do self.__dict__ assignments within the __init__ method of a dict-inherited class - just do the usual arg-passing to super() and then your processing. Actually i am not even sure what self.__dict__ = self implies - i can imagine that it will break some internal behaviour of dicts if you are overriding their __dict__ (even with "theirselfes").

Also it is better to modifiy the __getattr__ method, since this will be the fallback method for Python if __getattribute__ could not find anything. On the other hand, if you know that you're about to use most of the time the property-style accessor, then you could switch that logic.

See the following examples:

def string_empty_to_none(s):
  if type(s) == str:
    return None if (s.strip() == '') else s
  return s

# This will always have issues, even in IDE's
print('AttrDict')
class AttrDict(dict):
  def __init__(self, *args, **kwargs):
    super(AttrDict, self).__init__(*args, **kwargs)
    self.__dict__ = self
    self.clean_strings()
  def clean_strings(self):
    for key, value in self.items():
      self[key] = string_empty_to_none(value)

test = AttrDict({'a': 1})
test['x'] = 2
test.z = ""
print(test.a)
print(test['a'])
print(test.x)
print(test['x'])
print(test.z)
print(test['z'])


print('MostlyPropertiesAccessDict')
class MostlyPropertiesAccessDict(dict):
  def __init__(self, *args, **kwargs):
    # No need for the self.__dict__ part
    super().__init__(*args, **kwargs)
    self.clean_strings()

  def clean_strings(self):
    for key, value in self.items():
      self[key] = string_empty_to_none(value)

  def __getattr__(self, name):
    if not name in self:
      raise AttributeError(
        "Attribute {} does not exist".format(name))
    return self[name]

  def __setattr__(self, name, value):
    self[name] = string_empty_to_none(value)

  def __delattr__(self, name):
    if not name in self:
      raise AttributeError(
        "Attribute {} does not exist".format(name))
    del self[name]

test2 = MostlyPropertiesAccessDict({'a': 1})
test2['x'] = 2
test2.z = ""
print(test2.a)
print(test2['a'])
print(test2.x)
print(test2['x'])
print(test2.z)
print(test2['z'])

print("MostlyKeysAccessDict")
class MostlyKeysAccessDict(dict):
  def __init__(self, *args, **kwargs):
    # No need for the self.__dict__ part
    super().__init__(*args, **kwargs)
    self.clean_strings()

  def clean_strings(self):
    for key, value in self.items():
      self[key] = string_empty_to_none(value)

  def __getattribute__(self, name):
    if not name in self:
      raise AttributeError(
        "Attribute {} does not exist".format(name))
    return self[name]

  def __getattr__(self, name):
    return super().__getattribute__(name)

  def __setattr__(self, name, value):
    self[name] = string_empty_to_none(value)

  def __delattr__(self, name):
    if not name in self:
      raise AttributeError(
        "Attribute {} does not exist".format(name))
    del self[name]

test3 = MostlyKeysAccessDict({'a': 1})
test3['x'] = 2
test3.z = ""
print(test3.a)
print(test3['a'])
print(test3.x)
print(test3['x'])
print(test3.z)
print(test3['z'])
wiesion
  • 2,349
  • 12
  • 21
  • Great answer. The other answer (@bubble...) uses getitem/setitem. Is there any advantage to your method? Thank you for explaining how `__init__` could be improved. – 101010 Jun 13 '18 at 16:18
  • In general it does not really matter if you retrieve the values over `__getItem__(name)` or `self[name]` (same for `__setItem__(name, value)` and `self[name] = value`) the latter always calls the former internally in Python - and is the default way of not using internals when not needed for a specific reason. What actually did matter was the part with `__init__`. Also updated the code that when adding a value that it does your empty string check. – wiesion Jun 13 '18 at 16:37
0

All you need to do is create a wrapper class that contains a dictionary and then implement __getitem__:

class DictionaryWrapper():

    _dict = {"key1": "value1", "key2": "value2"}

    def __getitem__(self, item):
        return self._dict[item]

    @attribute
    def key1(self):
        return self._dict["key1"]

Now, you can treat your DictionartWrapper as a dictionary. (This limited implementation only allows reading.)

my_dictionary = DictionaryWrapper()
print(my_dictionary["key1"])

If the keys in your dictionary are known, you could even expose their values via attributes on the wrapper class.

print(my_dictionary.key1)

For a similar discussion, see here.

Eric McLachlan
  • 3,132
  • 2
  • 25
  • 37