Descriptors in python

Descriptor: a word or expression used to describe or identify something.

In python, descriptors are written to customized access of an object’s attribute. It involves three actions get, set and delete. Which are implemented using special __get__, __set__ and __delete__ functions respectively.

__get__: access the object’s instance value and returns it.
__set__: set object’s instance value and return nothing.
__delete__: delete object’s instance value and returns nothing.

Below is the example of a basic descriptor class.

class SimpleDescriptor(object):

    def __get__(self, instance, owner):
        """
        :param instance: self variable of the object being updated.
        :param owner: owning class object
        :return: None
        """
        return self.__dict__[instance]

    def __set__(self, instance, val):
        """
        :param instance: self variable of the object being updated.
        :param val: value of the object to be set
        :return: None
        """
        self.__dict__[instance] = val

    def __delete__(self, instance):
        """
        :param instance: self variable of the object being updated.
        :return: None
        """
        del self.__dict__[instance]

Using Descriptors

Descriptors can be helpful in many cases. One of the most common use is the data validation. For example, in a ranking application, rank value must be between number 1 to 10. This can be validated using descriptor in a clean way. Let’s create a new class, modify above code and see how it works.

class Item(object):
    rank = RankDescriptor()

    def __init__(self, name):
        self.name = name


class RankDescriptor(object):

    def __get__(self, instance, owner):...

    def __set__(self, instance, val):
        if not 1 <= val <= 10:
            raise ValueError(
                'Invalid rank {}, must be between 1 to 10.'.format(val)
            )
        self.__dict__[instance] = val

    def __delete__(self, instance):...

In Above example new class Item is added and RankDescriptor with modified __set__ function to handle data validation. A descriptor is attached to the class not to the instance. Therefore, a descriptor instance has to be added as a class variable and not as instance variable.

And to maintain data from different instances, descriptors need to maintain the dictionary of all instances. In the case above i am using builtin __dict__ attribute but it can also be maintained using new dictionary object created in descriptor itself.

Testing

import unittest

class TestItemRank(unittest.TestCase):

    def setUp(self):
        self.item = Item('Item-X')

    def test_rank_value_more_than_range(self):
        with self.assertRaises(ValueError):
            self.item.rank = 11

    def test_rank_value_less_than_range(self):
        with self.assertRaises(ValueError):
            self.item.rank = -11

    def test_rank_value_within_range(self):
        self.assertIsNone(setattr(self.item, 'rank', 9))

if __name__ == '__main__':
    unittest.main()

Unittest result

...
----------------------------------------------------------------------
Ran 3 tests in 0.000s

OK

All of our unit test passes means descriptor as data validator is working as expected.

Another example where descriptors can be handy is e-commerce application where product price need to be discounted on certain event. In this case __get__ method can be used to apply discount on product price upon checkout.

Let’s go through another example where descriptors are implemented to translate a message into different languages.



class EnglishToFrenchTranslator(object):

    DICTIONARY = {
        'Hello World!': 'Bonjour le monde!'
    }

    def __get__(self, instance, owner):
        # returns original message if no translation found
        return EnglishToFrenchTranslator.DICTIONARY.get(
            instance.msg, instance.msg
        )


class EnglishToGermanTranslator(object):

    DICTIONARY = {
        'Hello World!': 'Hallo Welt!'
    }

    def __get__(self, instance, owner):
        # returns original message if no translation found
        return EnglishToGermanTranslator.DICTIONARY.get(
            instance.msg, instance.msg
        )


class Message(object):
    in_french = EnglishToFrenchTranslator()
    in_german = EnglishToGermanTranslator()

    def __init__(self, msg):
        self.msg = msg


if __name__ == '__main__':
    msg = Message('Hello World!')
    print 'English ->', msg.msg
    print 'French ->', msg.in_french
    print 'German ->', msg.in_german

Output

English -> Hello World!
French -> Bonjour le monde!
German -> Hallo Welt!

Example above is fairly simple but good enough to extend out understating of descriptors.

We have defined a EnglishToFrenchTranslator and EnglishToGermanTranslator descriptors. Both of them implements a __get__ method to translate message to specific language.

The owner class, Message, has two attributes controlled by descriptor. Attribute in_french is the instance of EnglishToFrenchTranslator and in_german is the instance of EnglishToGermanTranslator. As you can see that descriptor can access the value of instance variable msg using instance parameter and doing further processing for translation.

Above code can also be found using these links:

 
0
Kudos
 
0
Kudos

Now read this

Python class &amp; static method

In this post i will try to explain python’s class and static method and how to use them. prerequisites # In order to understand class and static method, specially class method, one should know the difference between class and instance... Continue →