Thursday, August 2, 2007

Python: Typed attributes using descriptors

A few weeks a saw a post by David Stanek on Planet Python regarding using python descriptors to implement typed attributes. At the time, I needed something very similar and I've been trying to find something descriptors were good for (besides re-implementing the property builtin) so I decided to give his trick a try. The only problem was, I was coding on CalTrain at the time so I couldn't access his blog to reference the code in his post. The worst part was that I struggled with problems that, as it turns out, were pointed out in the comments to his posting (specifically, attributes implemented via descriptors would erroneously share state between instances of the classes the attributes were assigned to). By the time I got to the office and could consult David's blog post, this is the implementation I had working:
from collections import defaultdict

class TypedAttr(object):
"""Descriptor implementing typed attributes, converting
assigned values to the given type if necessary

Constructed with three parameters: the type of the attribute,
an initial value for the attribute, and an (optional)
function for converting values of other types to the desired
type.

If the converter function is not specified, then the type
factory is called to perform the conversion.
"""
__slots__ = ('__type', '__converter', '__value')
def __init__(self, type, initvalue=None, converter=None):
if converter is None:
converter = type
self.__type = type
self.__converter = converter
initvalue = self.convert(initvalue)
self.__value = defaultdict(lambda: initvalue)

def convert(self, value):
if not isinstance(value, self.__type) \
and value is not None:
value = self.__converter(value)
assert isinstance(value, self.__type) \
or value is None
return value

def __get__(self, instance, owner):
if instance is None:
return self
return self.__value[instance]

def __set__(self, instance, value):
self.__value[instance] = self.convert(value)

With this, I could write my classes like so:
class Example(object):
Name = TypedAttr(str)
Type = TypedAttr(str, "cheezy example")

Mainly I'm using the typed attributes for data validation in objects populated from values supplied by an untrusted source. It would be really nice if I could compose descriptors to build more complex managed attributes (sort of like you can with validators in Ian Bicking's FormEncode package). Then I could make a descriptor, for example, Unsigned or NotNone and compose them with TypedAttr like so:
class CompositionExample(object):
Name = TypedAttr(NotNone(str))
Age = Unsigned(TypedAttr(int))

I'll admit I haven't put a whole lot of thought into it yet, but at first glance it appears that it would be impossible to compose descriptors in python. I would love to be proven wrong.

6 comments:

toidinamai said...

A descriptor shouldn't store data itself: values are not cleaned up when an instance of a class that has such a descriptor is deleted. This could be fixed by changing the dictionary to a WeakKeyDictionary, provided that the classes allow weak references. Also your classes may be unhashable.

I also did such descriptors once and I generated slot names with a counter and used getattr(), setattr() and delattr() on the instance. This way you don't have the problems described above and even see the values when inspecting the objects with vars(). Another upside of using counters is that you know in which order the descriptors are declared.

Luke Plant said...

Regarding composition:

Does 'NotNone' or 'Unsigned' take a type and return a descriptor, or do they take descriptors and return descriptors? This is the fundamental confusion.

If you say that only 'TypedAttr' takes a type and returns a descriptor, then you are in luck. Make 'Unsigned' be a class factory that takes a type and returns a type with restrictions enforced in the constructor (or something). You can subclass builtin number types using the following.


def Unsigned(t):
__"""Takes a type an returns a subclass that forces it's value to be postive"""

__class _Unsigned(t):
____def __init__(self, x):
______if x < 0:
________raise Exception("Must be positive")
__return _Unsigned

Ian Bicking said...

It's a little awkward to figure out where you should store attributes. I've given up in a lot of cases and just passed the attribute name in, like "attr = SomeDescriptor('attr')". It makes the objects pickleable too, and other positive things.

You might find these descriptors interesting (there's also some in ohm.persist): http://svn.pythonpaste.org/Paste/OHM/trunk/ohm/descriptors.py

Anonymous said...

Python's descriptors are really great; they change the way you think about classes.

A more full-on extension of this concept can be found at

http://code.enthought.com/traits/

Kelly Yancey said...

Luke: Your Unsigned type doesn't cut it, you can still assign signed values to it after initialization. The only way to prevent that (that I am aware of) is to use descriptors since you could override the __set__ method to prevent assignment of signed values. But then that gets us back to the original question: is it possible to compose descriptors? I have yet to see evidence that you can.

Kelly Yancey said...

To the anonmyous poster suggesting the enthought traits package: thanks for the pointer. Interestingly, I don't see were the traits package allows composition of descriptors either (in fact, it looks like most of its magic happens in metaclasses). In any event, I see that descriptors take flags like "allow_none" in lieu of proper composition.