I needed something similar to, but not quite the same as,
collections.namedtuple.
The differences are that namedtuple requires the 'names' to be provided at
creation time, and then lends itself to creating multiple instances of
itself. I wanted a more generic class where I could supply the 'names' and
'values' at instantiation time.
I came up with a simple solution that seems to work -
>>> class MyTuple(tuple):
... def __new__(cls, names, values):
... for name, value in zip(names, values):
... setattr(cls, name, value)
... return tuple.__new__(cls, values)
...
>>> names = ['A', 'B', 'C']
>>> values = ['a', 'b', 'c']
>>>
>>> tup = MyTuple(names, values)
>>>
>>> print tup
('a', 'b', 'c')
>>>
>>> print tup[0]
a
>>>
>>> print tup.B
b
>>>
Then I had a need to add elements after the tuple had been created. As
tuples are immutable, I thought it would be easy to change it to a list.
However, it does not work -
>>> class MyList(list):
... def __new__(cls, names, values):
... for name, value in zip(names, values):
... setattr(cls, name, value)
... return list.__new__(cls, values)
...
>>> names = ['A', 'B', 'C']
>>> values = ['a', 'b', 'c']
>>>
>>> lst = MyList(names, values)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: list() takes at most 1 argument (2 given)
>>>
I can find a workaround, but I would be interested to know the reason why it
does not work.
Version is 2.6.2.
Thanks
Frank Millman
When subclassing immutable types, you need to override __new__;
otherwise you need to override __init__.
Here is an in-depth explanation: http://www.python.org/download/releases/2.2/descrintro/#metaclasses
Here is some code:
class MyTuple(tuple):
def __new__(cls, names, values):
for name, value in zip(names, values):
setattr(cls, name, value)
return tuple.__new__(cls, values)
class MyList(list):
def __init__(self, names, values):
list.__init__(self, values)
for name, value in zip(names, values):
setattr(self, name, value)
names = ['A', 'B', 'C']
values = ['a', 'b', 'c']
tup = MyTuple(names, values)
print tup
print tup[0]
print tup.B
lst = MyList(names, values)
print lst
print lst[0]
print lst.B
L.
> When subclassing immutable types, you need to override __new__;
> otherwise you need to override __init__.
Perfect. Thanks very much.
Frank
>>> lst[0] = "foobar"
>>> lst.A
'a'>>> lst.B = 42
>>> lst[1]
'b'
>>> lst.D="duh"
>>> lst[3]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
IndexError: list index out of range
> I came up with a simple solution that seems to work -
>
>>>> class MyTuple(tuple):
> ... def __new__(cls, names, values):
> ... for name, value in zip(names, values):
> ... setattr(cls, name, value)
> ... return tuple.__new__(cls, values)
> ...
>>>> names = ['A', 'B', 'C']
>>>> values = ['a', 'b', 'c']
>>>>
>>>> tup = MyTuple(names, values)
>>>>
Are you aware you are adding attributes to the class here, IOW:
MyTuple.C == 'c'
If you want to add attibutes to the instance:
class MyTuple(tuple):
def __new__(cls, names, values):
r = tuple.__new__(cls, values)
for name, value in zip(names, values):
setattr(r, name, value)
return r
names = ['A', 'B', 'C']
values = ['a', 'b', 'c']
tup = MyTuple(names, values)
assert tup[0] == 'a'
assert tup.B == 'b'
try:
MyTuple.C
except AttributeError:
pass
else:
assert False
Careful, this adds the new attributes as to the object's __dict__, not
to the tuple item slots. Which works ok if you don't care if that
someone can mutate the attributes, and if they do the attributes no
longer match the items. Personally I do mind.
tup = MyTuple(names,values)
assert tup.A == 'a'
assert tup[0] == 'a'
tup.A = 'd' # doesn't fail but should
assert tup.A == 'd'
assert tup[0] == 'd' # fails but, granted you allow mutabilty,
shouldn't
The best way to do what the OP wanted (originally) is this, no
subclassing necessary:
def my_named_tuple(names,values):
return namedtuple('ad_hoc_named_tuple',names)(*values)
As for the OP's second problem, to append named items, I'd first
consider whether I'm thinking about the problem correctly, and if so,
go with a subclass of list and overriding __getattr__. Probably one
of the rare cases I would override __getattr__ other than a proxy
class.
Carl Banks
>>>> class MyList(list):
> ... def __new__(cls, names, values):
> ... for name, value in zip(names, values):
> ... setattr(cls, name, value)
> ... return list.__new__(cls, values)
Did you really mean to setattr the class here? If I'm guessing
your intentions correctly, it should be:
def __new__(cls, names, values):
self = list.__new__(cls, values)
for name, value in zip(names, values):
setattr(self, name, value)
return self
> Traceback (most recent call last):
> File "<stdin>", line 1, in <module>
> TypeError: list() takes at most 1 argument (2 given)
>>>>
>
> I can find a workaround, but I would be interested to know the reason
> why it does not work.
Because you didn't define __init__, so MyList inherited the one from
list, and it has a different signature to what you're calling it with.
Simply add an __init__ with the proper signature, and the proper upcall
to list.__init__:
def __init__(self, names, values):
list.__init__(self, values)
But if you're doing that, you don't need __new__ at all. Simply
override __init__ and place your setattr loop there:
>>> class MyList(list):
... def __init__(self, names, values):
... for name, value in zip(names, values):
... setattr(self, name, value)
... list.__init__(self, values)
...
>>> MyList(['a'], [1])
[1]
>>> _.a
1
Thanks to all for the helpful replies.
Rob, you are correct, I had not realised I was adding attributes to the
class instead of the instance. Your alternative does work correctly. Thanks.
Carl, I understand your concern about modifying attributes. In my particular
case, this is not a problem, as the class is under my control, and an
instance will not be modified once it is set up, but I agree one must be
careful not to mis-use it.
My use-case is that I want to create a number of objects, I want to store
them in a tuple/list so that I can retrieve them sequentially, and I also
want to retrieve them individually by name. Normally I would create a tuple
and a dictionary to serve the two purposes, but I thought this might be a
convenient way to get both behaviours from the same structure.
Regarding adding elements after instantiation, I would subclass 'list', as
suggested by others, and then add an 'add' method, like this -
def add(self, name, value):
setattr(self, name, value)
self.append(value)
I tested this and it behaves as I want.
Having said all of this, I have realised that what I probably want is an
ordered dict. I will play with the one in PyPi, hopefully it will render
this entire discussion redundant. It was interesting, though, and I have
learned a lot.
Thanks again
Frank