Python Descriptors: A practical guide to understand the core
Descriptors are somewhat of an advanced concept, let’s understand the core of it with some real world example and not too much depth.
What are Python Descriptors?
Descriptor in python is an object which implements any of the method in the descriptor protocol. Descriptors are a powerful and useful protocol and behind some of the most useful concepts of python such as classmethods, staticmethods, properties etc.. The descriptor protocol is said to be the implementation of any of the below methods:
__get__(self, instance, type)
__set__(self, instance, value)
__delete__(self, instance)
That means any of the object which contains the implementation of any of the given methods, that object is said to be a descriptor. The object for the class DescriptorExample below will be a descriptor.
The output of the following code will be,
setting the value to 5
{}
As we instantiate the object of YourClass and in __init__ method we are trying to set the value of some_attr, and some_attr is the attribute which has been attached to the Descriptor and hence its behavior which is setting and retrieval will be overridden by the methods of the DescriptorExample.
Note that the value of some_attr has not been set as of yet, since we have just put the print statement in its get and set methods. And that’s why your_class_obj.__dict__ gives us an empty dict.
Let’s add the functionality to achieve the set and get behavior for the attribute of the instance.
The output of the following code will be,
setting the value to 5
getting the value of the object's attribute
5
setting the value to 10
getting the value of the object's attribute
10
As you can see, we have extended the behavior of getting and setting the some_attr attribute.
Types of Descriptors
- Data Descriptor: If an object defines __set__() or __delete__(), it is considered as a data descriptor. All the descriptors defined earlier are data descriptor as we have implemented __set__ method in them.
- Non Data Descriptor: If an object defines only __get__(), it is considered as a non data descriptor. We can also restrict the value assignment to the attribute.
Usage of Descriptor
Let’s say we have orders data with its order_id, shipping_price, and order_price. And we need to store the pretax values of shipping_price and order_price. Also, the tax calculated for Europe region and US region are different. In this example, we will be assuming the tax rates for EU and US region to be 19 and 12 respectively.
The usual way of doing this could be having a method to calculate pretax values of EU and US region. Something like this,
The output of the snippet is given below, as you can see we do get the pretax value after the first assignment of the attribute, but once the value of shipping_price is assigned again, we do not get the pretax value.
25.0
20
However the problem with this approach is if we assign a value to shipping_price or order_price later to the instance, it will not have a pretax value and instead the value which is assigned to it. Hence we will have sort of a broken class.
We can try to solve this problem by using descriptors, and having descriptors for both EU and US attributes. Something like this.
The output of the following will be, note that setting the value of shipping_price to be 11 and 22, we get the pretax values for both the attributes.
20.0
25.0
10.0
20.0
As you can see that we have reused the code for the marketplaces in the US, this way any new marketplace added for any of the US or EU region can simply attach their attribute whose pretax value need to be stored. This is nothing less than the magic of descriptors.
Descriptors can have a lot of other uses such as
- Data Validation — Validating the data before setting the value of the attribute.
- Restrict Access — As shown above, we can restrict the assignment of any attribute by using non data descriptor.
- Lazy Loading — We can put the business logic in the get method rather than set so we only compute while getting the data.
In this article, we tried not to go into much of depth how everything worked but enough to understand the concept of Data Descriptor and where it can be used. Do let me know your feedback if you feel something could have been added.