HiveBrain v1.2.0
Get Started
← Back to all entries
patternpythonMinor

Container (dict preserving key order, with attribute access)

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
dictordercontainerwithpreservingattributeaccesskey

Problem

This class is part of the Construct library.

Container is a dict that:

  • turns attribute access into key access so con.a -> con["a"]



  • preserves the order of keys



I know, this is similar to collections.OrderedDict but it isn't available in Python 2.6.

For anyone preferring more interactive code, you can pull the trunk and run nosetests.

```
def recursion_lock(retval="", lock_name="__recursion_lock__"):
def decorator(func):
def wrapper(self, *args, **kw):
if getattr(self, lock_name, False):
return retval
setattr(self, lock_name, True)
try:
return func(self, *args, **kw)
finally:
delattr(self, lock_name)

wrapper.__name__ = func.__name__
return wrapper

return decorator

class Container(dict):
r"""
A generic container of attributes.

Containers are dictionaries, translating attribute access into key access, and preserving key order.
"""
__slots__ = ["__keys_order__","__recursion_lock__"]

def __init__(self, *args, **kw):
object.__setattr__(self, "__keys_order__", [])
for arg in args:
for k, v in arg.items():
self[k] = v
for k, v in kw.items():
self[k] = v

def __getattr__(self, name):
try:
return self[name]
except KeyError:
raise AttributeError(name)

def __setitem__(self, key, val):
if key not in self:
self.__keys_order__.append(key)
dict.__setitem__(self, key, val)

def __delitem__(self, key):
dict.__delitem__(self, key)
self.__keys_order__.remove(key)

__delattr__ = __delitem__
__setattr__ = __setitem__

def __call__(self, **kw):
for k,v in kw.items():
self.__setitem__(k, v)
return self

def clear(self):
dict.clear(self)
del self.__keys_order__[:]

def pop(self, key, *default):

Solution

While the performance of your container seems to be not your major concern, it is always nice to know where you are standing. Therefore I would compare it to this implementation of an OrderedDict for 2.4 <= python <= 2.6.

In addition, what your container seems to lack is to give it an ordered sequence on initialization. It can only take an iterable which has the method items defined. But it would be nice to be able to give it an iterable of 2-tuples, so something like:

c = Container([("a", 1), ("b", 2), ("c", 3), ...])


collections.OrderedDict offers this, therefore a user might expect your implementation to also have it. Without it, the ordering will not give you any advantage, because as soon as you do:

c = Container({"a": 1, "b": 2, "c": 3, ...})
c = Container(a=1, b=2, c=3, ...)


you loose the order, because of python dicts disorderedness. You try to get around this with __call__, allowing you to do:

c = Container(a=1)(b=2)(c=3)


but in my opinion this is less clean and clear than using e.g. a list of tuples. The latter also simplifies your copy:

def copy(self):
    return inst = self.__class__(self.iteritems())

__copy__ = copy


The implementation for this is actually already in your update method.

I also added __copy__ = copy here to allow usage of the copy module.

In your _search method, you are doing a return None at the end. This is superfluous as that is the default return value of a python function.

It is better to ask forgiveness than permission, so I would use try..except in update:

def update(self, seq, **kw):
    try:
        for k in seq.keys():
            self[k] = seq[k]
    except AttributeError:
        for k, v in seq:
            self[k] = v
    dict.update(self, kw)


I would also think about which you want to put in the try clause (the one that fails less often, which depends on you usage of this class).

dict.pop() (without a key) is not defined, because the 'last' element in a dict is of course not well-defined. For an ordered dict, it might make sense, however, to allow it (and collections.OrderedDict does, with its pop method):

def pop(self, key=None, *default):
    if not key:
        key = self.__keys_order__.pop()
    else:
        self.__keys_order__.remove(key)
    val = dict.pop(self, key, *default)
    return val


This takes advantage of the fact that list.pop is O(1), whereas list.remove is O(n) if the last element is the one you want to remove (because it needs to search through the whole list to find the last element).

Your popitem also seems to be randomly ordered, because it directly uses dict.popitem. It should be:

def popitem(self):
    k = self.__keys_order__.pop()
    v = dict.pop(self, k)
    return k, v

Code Snippets

c = Container([("a", 1), ("b", 2), ("c", 3), ...])
c = Container({"a": 1, "b": 2, "c": 3, ...})
c = Container(a=1, b=2, c=3, ...)
c = Container(a=1)(b=2)(c=3)
def copy(self):
    return inst = self.__class__(self.iteritems())

__copy__ = copy
def update(self, seq, **kw):
    try:
        for k in seq.keys():
            self[k] = seq[k]
    except AttributeError:
        for k, v in seq:
            self[k] = v
    dict.update(self, kw)

Context

StackExchange Code Review Q#140500, answer score: 4

Revisions (0)

No revisions yet.