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.'