-
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/setattrbehavior (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.'