__all__ = ['fkdict']
__author__ = 'George Sakkis <george.sakkis AT gmail DOT com>'
import UserDict
from itertools import chain,izip
class fkdict(object):
'''A dict-like class for mappings with fixed keys.
The main feature of this class is memory efficiency in the scenario of
having many dicts with the same keys. Example use cases are rows read from a
csv.DictReader or constructed out of rows fetched from a database. All such
rows share the same keys() and therefore each row has to only hold the
values().
An additional feature is predictable ordering:
fkdict.fromkeys('abcd').keys() == list('abcd')
Currently fkdict does not support the dict operations that change the size
of the container. Therefore, __delitem__(), pop(), popitem(), clear() and
setdefault() are not provided. Also __setitem__() raises KeyError if the key
does not already exist.
'''
__slots__ = ['_values']
def __init__(self, seq=(), **kwds):
# normalize arguments to a (key,value) iterable
if hasattr(seq, 'keys'):
get = seq.__getitem__
seq = ((k,get(k)) for k in seq.keys())
if kwds:
seq = chain(seq, kwds.iteritems())
# scan the items keeping track of the keys' order
keys,values = [],[]
keys_set = set()
for k,v in seq:
if k not in keys_set:
keys_set.add(k)
keys.append(k)
values.append(v)
else:
values[keys.index(k)] = v
# mutate self to the appropriate subclass
self.__class__ = self._subcls_factory(*keys)
self._values = values
@classmethod
def fromkeys(cls, keys, default=None):
self = cls()
self.__class__ = cls._subcls_factory(*keys)
self._values = [default] * len(self)
return self
def __len__(self):
return len(self._keys)
def __contains__(self, key):
return key in self._key2index
def has_key(self, key):
return key in self._key2index
def __getitem__(self, key):
return self._values[self._key2index[key]]
def get(self, key, default=None):
try: return self[key]
except KeyError: return default
def __setitem__(self, key, value):
self._values[self._key2index[key]] = value
def update(self, other=None, **kwargs):
# copied from UserDict.DictMixin
# Make progressively weaker assumptions about "other"
if other is None:
pass
elif hasattr(other, 'iteritems'):
for k, v in other.iteritems():
self[k] = v
elif hasattr(other, 'keys'):
for k in other.keys():
self[k] = other[k]
else:
for k, v in other:
self[k] = v
for k, v in kwargs.iteritems():
self[k] = v
def __iter__(self):
return iter(self._keys)
def iterkeys(self):
return iter(self._keys)
def itervalues(self):
return iter(self._values)
def iteritems(self):
return izip(self._keys, self._values)
def keys(self):
return list(self._keys)
def values(self):
return list(self._values)
def items(self):
return zip(self._keys, self._values)
def copy(self):
return self.__class__(self.iteritems())
def __cmp__(self, other):
# copied from UserDict.DictMixin
if other is None:
return 1
if isinstance(other, UserDict.DictMixin):
other = dict(other.iteritems())
return cmp(dict(self.iteritems()), other)
def __repr__(self):
return '{%s}' % ', '.join('%r: %r' % item for item in self.iteritems())
@classmethod
def _subcls_factory(cls, *keys):
# if cls is hidden, find its first non hidden ancestor
cls = (c for c in cls.mro() if not issubclass(c,_Hidden)).next()
# each (non hidden) class maintains its own registry
try: registry = cls.__dict__['_Registry']
except KeyError:
registry = cls._Registry = {}
try: return registry[keys]
except KeyError:
cls_name = '%s_%d' % (cls.__name__, abs(hash(keys)))
cls_dict = {
'__slots__' : (),
'_keys' : keys,
'_key2index' : dict((k,i) for i,k in enumerate(keys))
}
registry[keys] = sub = type(cls_name, (cls,_Hidden), cls_dict)
return sub
class _Hidden(object):
__slots__ = ()