It looks like Python is getting tuples with named members in 2.6 (http://www.oluyede.org/blog/2007/03/11/updates-from-python-svn-part-2/ and http://docs.python.org/dev/lib/named-tuple-factory.html). I suspect many of us have implemented similar functionality ourselves, e.g. Shannon -jj Behrens describes how he sometimes uses dictionaries to return composite polymorphic values (http://jjinux.blogspot.com/2007/03/python-returning-multiple-things-of.html). The problem with dictionaries is of course that they require too much excercise of your little finger in typing [‘xx’]. That’s even worse for me since I’m using a keyboard layout that switches national characters onto those keys when I tap the caps-lock key, so I can use my American-keyboard touch typing skillz and eat my national characters as well (I’m looking forward to the Metaphor-off!)
It looks like the new Python NamedTuple type is going to limit the fields to those that are defined at creation time. It’s based on a tuple, so I suppose that follows naturally, however it doesn’t seem natural for the abstract-data-type of a container of named fields with iteration and indexing. I’ve called my implementation of this ADT a property set since most of the motivating use cases for this was returning returning values that had properties attached to them. The use is as follows…
You can assign to random fields, the only limitation is that they cannot start with an underscore, but public fields wouldn’t have that anyway so it’s not really a limitation (the limitation comes from the fact that the implementation overrides __setattr__ and being able to interpret fields starting with an underscore as internal to the implementation simplifies things quite a bit):
[sourcecode language=”Python”]
>>> p = pset()
>>> p.a = 42
>>> p.b = ‘hello’
>>> p.c = [p.a, p.b]
>>> p
pset(a=42, b=’hello’, c=[42, ‘hello’])
[/sourcecode]
You can iterate over the values:
[sourcecode language=”Python”]
>>> for key, value in p:
… print key, value
…
a 42
b hello
c [42, ‘hello’]
[/sourcecode]
Notice that it maintains the insertion order, and you can also access by index:
[sourcecode language=”Python”]
>>> p[1]
‘hello’
[/sourcecode]
For technical reasons it is not possible to maintain the order when creating a pset from keyword arguments (I was hesitating to put this functionality in, but practicality beats purity, and it’s turned out to be very practical). Equality does not require isomorphism, which means that as long as the sets have the same fields they compare equal:
[sourcecode language=”Python”]
>>> q = pset(a=42, b=’hello’, c=[42,’hello’])
>>> q
pset(a=42, c=[42, ‘hello’], b=’hello’)
>>> p == q
True
[/sourcecode]
You can keep the order given to the constructor by initializing with a list of tuples:
[sourcecode language=”Python”]
>>> list(p.items())
[(‘a’, 42), (‘b’, ‘hello’), (‘c’, [42, ‘hello’])]
>>> r = pset(p.items())
>>> r
pset(a=42, b=’hello’, c=[42, ‘hello’])
[/sourcecode]
The example above does of course not mean that you can’t create a pset from a pset directly (this also maintains order):
[sourcecode language=”Python”]
>>> s = pset(p)
>>> s
pset(a=42, b=’hello’, c=[42, ‘hello’])
[/sourcecode]
It’s also extremely useful to be able to use field indexing notation as well:
[sourcecode language=”Python”]
>>> p
pset(a=42, b=’hello’, c=[42, ‘hello’])
>>> p.b
‘hello’
>>> p[1]
‘hello’
>>> p[‘b’]
‘hello’
>>> p[‘b’] = ‘world’
>>> p
pset(a=42, b=’world’, c=[42, ‘hello’])
>>> p[1] = ‘foo’
>>> p
pset(a=42, b=’foo’, c=[42, ‘hello’])
[/sourcecode]
Here’s the code:
[sourcecode language=”Python”]
class pset(dict):
"""This code is placed in the Public Domain.
Property Set class.
A property set is an object where values are attached to attributes,
but can still be iterated over as key/value pairs.
The order of assignment is maintained during iteration.
Only one value allowed per key.
>>> x = pset()
>>> x.a = 42
>>> x.b = ‘foo’
>>> x.a = 314
>>> x
pset(a=314, b=’foo’)
"""
def __init__(self, items=(), **attrs):
object.__setattr__(self, ‘_order’, [])
super(pset, self).__init__()
for k, v in items:
self.add(k, v)
for k, v in attrs.items():
self.add(k, v)
def add(self, key, value):
if type(key) in (int, long):
key = self._order[key]
elif key not in self._order:
self._order.append(key)
dict.__setitem__(self, key, value)
def __eq__(self, other):
"""Equal iff they have the same set of keys, and the values for
each key is equal. Key order is not considered for equality.
"""
if set(self._order) == set(other._order):
for key in self._order:
if self[key] != other[key]:
return False
return True
return False
def __iadd__(self, other):
for k, v in other:
self.add(k, v)
# should probably have an __radd__ method too…
def __add__(self, other):
tmp = self.__class__()
tmp += self
tmp += other
return tmp
def __repr__(self):
vals = ‘, ‘.join(‘%s=%s’ % (k, repr(v)) for (k,v) in self)
return ‘%s(%s)’ % (self.__class__.__name__, vals)
def __getattr__(self, key):
if key not in self:
raise AttributeError(key)
return self.get(key)
def __getitem__(self, key):
if type(key) in (int, long):
key = self._order[key]
return self.get(key)
__str__ = __repr__
def __iter__(self):
return ((k, self.get(k)) for k in self._order)
def items(self):
return iter(self)
def __setattr__(self, key, val):
if key.startswith(‘_’):
object.__setattr__(self, key, val)
else:
self.add(key, val)
def __setitem__(self, key, val):
self.add(key, val)
[/sourcecode]