Descriptors were introduced to Python way back in version 2.2. They provide the developer with the ability to add managed attributes to objects. The methods needed to create a descriptor are __get__, __set__ and __delete__. If you define any of these methods, then you have created a descriptor.
The idea behind the descriptor is to get, set or delete attributes from your object’s dictionary. When you access a class attribute, this starts the lookup chain. Should the looked up value be an object with one of our descriptor methods defined, then the descriptor method will be invoked.
Descriptors power a lot of the magic of Python’s internals. They are what make properties, methods and even the super function work. They are also used to implement the new style classes that were also introduced in Python 2.2.
The Descriptor Protocol
The protocol to create a descriptor is really quite easy. You only need to define one or more of the following:
- __get__(self, obj, type=None), returns value
- __set__(self, obj, value), returns None
- __delete__(self, obj), returns None
Once you’ve defined at least one, you have created a descriptor. If you can you define both __get__ and __set__, you will have created a data descriptor. A descriptor with only __get__() defined are known as non-data descriptors and are usually used for methods. The reason for this distinction in descriptor types is that if an instance’s dictionary happens to have a data descriptor, the descriptor will take precedence during the lookup. If the instance’s dictionary has an entry that matches up with a non-data descriptor, then the dictionary’s own entry will take precedence over the descriptor.
You can also create a read-only descriptor if you define both __get__ and __set__, but raise an AttributeError when the __set__ method is called.