Jan 29

Python instance descriptors: when class descriptors aren't dynamic enough

Python descriptors are great for customizing access to attributes on a class or instance. They are a big win for tasks like mapping Python objects to data from non-Python sources (such as SQL), since mapped attributes will need to be encoded/decoded and connected to other attributes in some way.

Below is a very simple descriptor; as you can see, accessing it from both the class and the instance invoke the descriptor protocol:

class Test(object):
    pass

class Descriptor(object):
    def __get__(self, instance, owner):
        return "Hello, world."

>>> Test.x = Descriptor()
>>> Test.x
'Hello, world.'
>>> test = Test()
>>> test.x
'Hello, world.'

However, in order to add descriptors to an object, they must be added to the object’s class. Descriptors added to an instance do not invoke the descriptor protocol:

>>> test.y = Descriptor()
>>> test.y
<__main__.Descriptor object at 0x16fe810>

This means that creating an instance with dynamic (determined at runtime) descriptors requires either the heavy-handed approach of generating a class just for that object (since adding descriptors to its class will add them to all other instances of the class), or the ad-hoc approach of redefining getattr/setattr behavior (essentially re-implementing your own descriptor protocol).

It turns out the latter approach is not as messy as it first sounds. Below is a class that enables “instance descriptors”:

class InstanceDescriptorMixin(object):
    def __getattribute__(self, name):
        value = object.__getattribute__(self, name)
        if hasattr(value, '__get__'):
            value = value.__get__(self, self.__class__)
        return value

    def __setattr__(self, name, value):
        try:
            obj = object.__getattribute__(self, name)
        except AttributeError:
            pass
        else:
            if hasattr(obj, '__set__'):
                return obj.__set__(self, value)
        return object.__setattr__(self, name, value)

class Test(InstanceDescriptorMixin):
    pass

>>> test = Test()
>>> test.z = Descriptor()
>>> test.z
'Hello, world.'
Page 1 of 1