2007-02-25
Ordered lists with named fields for Python
I periodically find myself dealing with structures that are basically ordered lists with named fields, where elements 0, 1, and 2 are naturally named 'a', 'b', and 'c' and sometimes you want to refer to them by name instead of having to remember their position. This pattern even crops up in the standard Python library, often with functions that started out just returning an ordered list and grew the named fields portion later as people discovered how annoying it was to have to remember that the hour was field 3.
This being Python, I've built myself some general code to add named
fields on top of sequence types like list or tuple. For maximum
generality my code supports using field names both as attribute names
and as indexes, so you can use both obj.field and obj["field"], and
you can even do crazy things like obj["field":-1]. The code:
class GetMixin(object):
fields = {}
def _mapslice(self, key):
s, e, step = key.start, key.stop, key.step
if s in self.fields:
s = self.fields[s]
if e in self.fields:
e = self.fields[e]
return slice(s, e, step)
def _mapkey(self, key):
if isinstance(key, tuple):
pass
elif isinstance(key, slice):
key = self._mapslice(key)
elif key in self.fields:
key = self.fields[key]
return key
def __getitem__(self, key):
key = self._mapkey(key)
return super(GetMixin, self).__getitem__(key)
def __getattr__(self, name):
if name in self.fields:
return self[self.fields[name]]
raise AttributeError, \
"object has no attribute '%s'" % name
class SetMixin(GetMixin):
def __setitem__(self, key, value):
key = self._mapkey(key)
super(SetMixin, self).__setitem__(key, value)
def __setattr__(self, name, value):
if name in self.fields:
o = self.fields[name]
self[o] = value
else:
self.__dict__[name] = value
class Example(SetMixin, list):
fields = {'a': 0, 'b': 1, 'c': 2}
The fields class variable is a dictionary mapping the names of the
fields to their index offsets; it need not include all fields, and
not all named fields necessarily have values for a particular list
(since nothing checks the list length).
GetMixin just lets you read the named fields and can be mixed in with
tuples; SetMixin lets you write to them by name too, and so needs to
be mixed in with lists or other writable sequence types.
The easiest way to generate the fields value for the usual case
of sequential field names starting from the first element of the list
is to use a variant of the enumerate function from the itertools
recipes:
from itertools import *
def enum_args(*args):
return izip(args, count())
class Example(SetMixin, list):
fields = dict(enum_args('a', 'b', 'c'))
(If you're going to do this a lot, make a version of enum_args that
does the dict() step too.)
Inheriting from list, tuple, etc does have one practical wart: you
probably want to avoid using field names that are the names of methods
that you want to use, because you won't be able to use the obj.field
syntax for accessing them. Amusingly, you will be able to set them
using that syntax, because __setattr__ gets called for everything,
existing attributes included (which is why it needs the dance at the
end with the instance's __dict__).
(This code is not quite neurotically complete; truly neurotically
complete code would make the available fields appear in dir()'s
output. But I don't want to try to think what sort of hacks that
would take, since I am seeing visions of dancing metaclasses that
automatically create properties for each field name.)