Python :: property set

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

      >>> p = pset()
      >>> p.a = 42
      >>> p.b = 'hello'
      >>> p.c = [p.a, p.b]
      >>> p
      pset(a=42, b='hello', c=[42, 'hello'])

You can iterate over the values:

      >>> for key, value in p:
      ...     print key, value
      ...
      a 42
      b hello
      c [42, 'hello']

Notice that it maintains the insertion order, and you can also access by index:

      >>> p[1]
      'hello'

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:

      >>> q = pset(a=42, b='hello', c=[42,'hello'])
      >>> q
      pset(a=42, c=[42, 'hello'], b='hello')
      >>> p == q
      True

You can keep the order given to the constructor by initializing with a list of tuples:

      >>> list(p.items())
      [('a', 42), ('b', 'hello'), ('c', [42, 'hello'])]
      >>> r = pset(p.items())
      >>> r
      pset(a=42, b='hello', c=[42, 'hello'])

The example above does of course not mean that you can’t create a pset from a pset directly (this also maintains order):

      >>> s = pset(p)
      >>> s
      pset(a=42, b='hello', c=[42, 'hello'])

It’s also extremely useful to be able to use field indexing notation as well:

      >>> 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'])

Here’s the code:

      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)
This entry was posted in Python. Bookmark the permalink.

Leave a Reply

Your email address will not be published. Required fields are marked *