wap26
wap26

Reputation: 2290

Accessing OrderedDict keys like attributes in Python

I want to write a container class which

  1. allows both indexing (in the dictionary style), e.g. data['a'] and attribute-like access e.g. data.a ; this is addressed here
  2. preserves the order in which entries where added, e.g. by subclassing collections.OrderedDict ; this is addressed here

I adapted the solution for 1. to subclass collections.OrderedDict instead of dict but it does not work; see below.

from collections import OrderedDict

class mydict(OrderedDict):
    def __init__(self, *args, **kwargs):
        super(mydict, self).__init__(*args, **kwargs)
        self.__dict__ = self

D = mydict(a=1, b=2)

#D['c'] = 3 # fails
D.d    = 4

#print D    # fails

The two lines with the failure comment cause the following errors:

    print D
  File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/collections.py", line 176, in __repr__
    return '%s(%r)' % (self.__class__.__name__, self.items())
  File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/collections.py", line 113, in items
    return [(key, self[key]) for key in self]
  File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/collections.py", line 76, in __iter__
    root = self.__root
AttributeError: 'struct' object has no attribute '_OrderedDict__root'

and

    D['c'] = 3
  File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/collections.py", line 58, in __setitem__
    root = self.__root
AttributeError: 'mydict' object has no attribute '_OrderedDict__root'

Is there a solution to this ? Can the __init__ function be patched ?

Upvotes: 4

Views: 4141

Answers (1)

abarnert
abarnert

Reputation: 365925

First, if you just want to get this working, not understand what's wrong with your attempt, there are multiple "AttrDict" implementations on PyPI, ActiveState, etc. Search for one and look at its code—or just install it and use it.


That self.__dict__ = self is not what you want. I realize that people recommend it, but it has some fundamental major problems. And one of those problems is exactly the one you're seeing.

In particular, it means you've now lost all of your existing attributes, including internal attributes used by OrderedDict itself (like that __root that you're getting errors about) and special attributes attached to all objects (like __class__).

When you're just using dict you don't notice this problem—at least in CPython—because dict is implemented in C, and the special members that it needs to operate are stored in a C struct, not in the __dict__, so you can't lose them. But for any class implemented in Python that needs any special members, the problem becomes visible.


Python has special methods for customizing attribute access. What you want to do is implement those methods:

class AttrDict(OrderedDict):
    def __getattr__(self, name):
        return self[name]
    def __setattr__(self, name, value):
        self[name] = value
    def __delattr__(self, name):
        del self[name]

However, in this case, even that won't work for everything, because you're still going to get your overrides called when the code in OrderedDict tries to use __root! To fix that you need to forward only some calls in these methods to your dict, not all of them.

And that "some" is complicated and hard to get right. A much simpler solution would be to just encapsulate and delegate to an OrderedDict instead of inheriting:

class AttrDict(object):
    def __init__(self, *args, **kwargs):
        self._od = OrderedDict(*args, **kwargs)
    def __getattr__(self, name):
        return self._od[name]
    def __setattr__(self, name, value):
        if name == '_od':
            self.__dict__['_od'] = value
        else:
            self._od[name] = value
    def __delattr__(self, name):
        del self._od[name]

Or, more simply, just use an OrderedDict as your __dict__—which is the paradigm case for a metaclass, but if you want to do it quick&dirty, you can do it in __init__ or __new__ instead.


The latter solutions just make you into a normal object with attributes that happen to be ordered, not a mapping at all. But it's pretty easy to forward the mapping methods as well. Implement __getitem__, __setitem__, __delitem__, __iter__, and __len__ to forward to self._od, and inherit from collections.MutableMapping to fill in the rest of the API.

And now, if you go back and look up "AttrDict" on PyPI and click on the implementations, you'll see that this is exactly what they all do: they forward both __getattr__ and __getitem__ to an OrderedDict's __getitem__, and so on.

Upvotes: 8

Related Questions