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.)
|
|