I required a type similar to namedtuple
, but with changeable fields. The namedlist
should behave as follows:
Something = namedlist('Something', ['a', 'b'])
st = Something(123, 456)
print st # Something(a=123, b=456)
a, b = st
print a, b, st.a, st.b # 123 456 123 456
st.a = 777
print st # Something(a=777, b=456)
st.b = 888
print st # Something(a=777, b=888)
After learning a bit about descriptors here and there, I got the following code working as described above:
class IndexAccessor(object):
def __init__(self, index):
self.index = index
def __get__(self, instance, owner):
return list.__getitem__(instance, self.index)
def __set__(self, instance, value):
list.__setitem__(instance, self.index, value)
def namedlist(classname, fields):
def init(self, *args):
list.__init__(self, args)
def repr(self):
return '%s(%s)' % (self.__class__.__name__, ', '.join(['%s=%s' % (k,list.__getitem__(self, i)) for i, k in enumerate(self._fields)]))
attrs = {'_fields': tuple(fields),
'__init__': init,
'__repr__': repr
}
for index, field in enumerate(fields):
attrs[field] = IndexAccessor(index)
return type(classname, (list,), attrs)
The IndexAccessor
class feels a bit like boilerplate to me, so I am wondering whether the code could be improved to avoid it.
I tried to replace the for index, field in enumerate(fields)
loop with this
for index, field in enumerate(fields):
def get(self):
return list.__getitem__(self, index)
def set(self, v):
list.__setitem__(self, index, v)
attrs[field] = property(get, set, None, 'Property "%s" mapped to item %d' % (field, index))
...but that caused the print a, b, st.a, st.b
statement to produce 123 456 456 456
instead of 123 456 123 456
, which I suspect has something to do with the index
not working as required.
Bonus points for answers which can also provide concise code to allow name-based assignment in the constructor as in
Something = namedlist('Something', ['a', 'b'])
st = Something(a=123, b=456)
The reason that the for loop doesn't work is that the index
variable referenced in the get
and set
functions is not copied. It is a reference to the index
variable that gets set in the for loop. So all attribute accesses get the last value. This can be solved by explicitly storing the index as a default parameter.
Fixed code:
for index, field in enumerate(fields):
def get(self,index=index):
return list.__getitem__(self, index)
def set(self, v,index=index):
list.__setitem__(self, index, v)
attrs[field] = property(get, set, None, 'Property "%s" mapped to item %d' % (field, index))
And to allow keyword arguments, modify your __init__
function to the following:
def init(self, *args, **kwargs):
kwargs.update(zip(self._fields, args))
args = [kwargs[k] for k in self._fields]
list.__init__(self, args)