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.