1

Background

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)

Question

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)
FriendFX
  • 2,929
  • 1
  • 34
  • 63
  • Taking the approach that `namedtuple` itself does, you create a class template string in `namedlist`, apply the given fields to the template, then `exec` the resulting string to produce the actual class. – chepner Nov 02 '15 at 00:52
  • If this is working code that you think could be improved, see [codereview.se]. If not, please clarify the issue. – jonrsharpe Nov 02 '15 at 00:52
  • @jonrsharpe I disagree, as the question would still make sense if the working version of `namedlist` were removed. – pppery Nov 02 '15 at 00:56
  • @jonrsharpe: The code as it stands in Background does work, except for the name-based assignments in the construction. I could also remove the `IndexAccessor` class and replace the `for` loop as described under Question, which would break the code. Would that be preferable over moving the question to Code Review? – FriendFX Nov 02 '15 at 00:56

2 Answers2

1

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)
FriendFX
  • 2,929
  • 1
  • 34
  • 63
pppery
  • 3,731
  • 22
  • 33
  • 46
0

I took a different route to solving this problem which may be worth posting alongside yours. It is more general--which may or may not be desirable--and shorter but it might also be inefficient.

class Struct:
    def __init__(self):
        pass
    def __getattribute__(self, key):
        return super().__getattribute__(key)
    def __setattribute__(self, key, value):
        super().__setattribute__(key, value)
Lemma Prism
  • 371
  • 1
  • 2
  • 8