Skip to content

Descriptor

Python has a concept called descriptor. But how and when should we use it?

Without Descriptor

Counts and Last Read/Update Time

For example, we have a class UnderneathAll, it has an attribute called website.

class UnderneathAll:
    website = None

We want to record:

  • the number of times the attribute is accessed
  • the last time it is accessed
  • the last time it is updated.

Not a big problem, rewrite the class with @property and @website.setter will solve the problem.

import datetime


class UnderneathAll:
    _website_access_count = 0

    @property
    def website(self):
        self._website_access_count += 1
        self._website_last_read_time = datetime.datetime.now()
        return self._website

    @website.setter
    def website(self, value):
        self._website_last_update_time = datetime.datetime.now()
        self._website = value
        return self._website
print('='*50)
ua  = UnderneathAll()
ua.website = 'www.underneathall.app'
print('Website:', ua.website)
print('Last Read Time:', ua._website_last_read_time)
print('Access Counts:', ua._website_access_count)
print('='*50)
ua.website = 'www.underneathall.com'
print('Website:', ua.website)
print('Last Update Time:', ua._website_last_update_time)
print('Last Read Time:', ua._website_last_read_time)
print('Access Counts:', ua._website_access_count)
print('='*50)
==================================================
Website: www.underneathall.app
Last Read Time: 2021-02-25 08:02:33.955667
Access Counts: 1
==================================================
Website: www.underneathall.com
Last Update Time: 2021-02-25 08:02:33.956255
Last Read Time: 2021-02-25 08:02:33.956324
Access Counts: 2
==================================================

Yes, the problem is solved.

However, if we have another attribute author, and we also want to record:

  • the number of times the attribute is accessed
  • the last time it is accessed
  • the last time it is updated.

Tip

What if there are more attributes we want to track? You may ask: why not override __getattribute__ and __setattr__ methods?

If we only want to track part of all the attributes, overriding these two methods may mess up your codes.

Well, with a careful design, I believe you can come up with some functions/classes that can deal with the situation without messing up your code base.

But we've got something better: a descriptor.

Descriptor

A descriptor is a class that has implemented __get__, __set__, and optionally __set_name__.

Let's have a look how to solve our problem using descriptor.

Counts and Last Read/Update Time With Descriptor

import datetime


class UnderneathAllDescriptor:
    def __set_name__(self, instance, name):
        self._instance_attr = f'_{name}'
        self._count_attr = f'_{name}_access_count'
        self._last_read_time = f'_{name}_last_read_time'
        self._last_update_time = f'_{name}_last_update_time'

    def __get__(self, instance, objtype=None):
        setattr(instance, self._count_attr, getattr(instance, self._count_attr, 0) + 1)
        setattr(instance, self._last_read_time, datetime.datetime.now())
        return getattr(instance, self._instance_attr)

    def __set__(self, instance, value):
        setattr(instance, self._instance_attr, value)
        setattr(instance, self._last_update_time, datetime.datetime.now())
        return True

class UnderneathAll:
    website = UnderneathAllDescriptor()
    author = UnderneathAllDescriptor()
print('='*50)
ua  = UnderneathAll()
ua.website = 'www.underneathall.app'
print('Website:', ua.website)
print('Last Read Time:', ua._website_last_read_time)
print('Access Counts:', ua._website_access_count)
print('='*50)
ua.website = 'www.underneathall.com'
print('Website:', ua.website)
print('Last Update Time:', ua._website_last_update_time)
print('Last Read Time:', ua._website_last_read_time)
print('Access Counts:', ua._website_access_count)
print('='*50)
ua.author = 'William WANG'
print('Author:', ua.author)
print('Last Read Time:', ua._author_last_read_time)
print('Access Counts:', ua._author_access_count)
print('='*50)
ua.author = 'Jiuhe WANG'
print('Author:', ua.author)
print('Last Update Time:', ua._author_last_update_time)
print('Last Read Time:', ua._author_last_read_time)
print('Access Counts:', ua._author_access_count)
print('='*50)
==================================================
Website: www.underneathall.app
Last Read Time: 2021-02-25 08:20:13.593266
Access Counts: 1
==================================================
Website: www.underneathall.com
Last Update Time: 2021-02-25 08:20:13.593753
Last Read Time: 2021-02-25 08:20:13.593801
Access Counts: 2
==================================================
Author: William WANG
Last Read Time: 2021-02-25 08:20:13.594403
Access Counts: 1
==================================================
Author: Jiuhe WANG
Last Update Time: 2021-02-25 08:20:13.594813
Last Read Time: 2021-02-25 08:20:13.594859
Access Counts: 2
==================================================

How does it work

A descriptor can customize the behavior retrieving or updating an attribute.

As an attribute of an instance, when defined, the instance and the attribute name is passed to __set_name__ methods

The the attribute is accessed, the __get__ method is called, and when it gets updated, the __set__ method is called.

Yes, that is just how simple it is.