Descriptors
As the documentation tells us, a descriptor is just something that implements the __get__
, __set__
or __delete__
method(s). You’ve probably used them yourself and not known it - examples include classmethod
, staticmethod
and property
decorators (though they are likely implemented in C or C++, not in python). You call them inside classes and can use them as decorators in handy ways (see my previous post about properties for example).
You can also implement one yourself. Examples in which they’re often then implemented are as validators. These allow you to ensure certain conditions are met for your class attributes (such as a number being above 0 or a string having to be one of a set numbers of options).
The below Validator class is from the HowTo page for Descriptors and has excellent examples. One frequent use I’ve had for validation has been things like date formats or where variables need to be one of a set of strings.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import datetime
from abc import ABC, abstractmethod
class Validator(ABC):
def __set_name__(self, owner, name):
self.private_name = '_' + name
def __get__(self, obj, objtype=None):
return getattr(obj, self.private_name)
def __set__(self, obj, value):
self.validate(value)
setattr(obj, self.private_name, value)
@abstractmethod
def validate(self, value):
pass
class DateFormat(Validator):
def validate(self, value):
try:
datetime.datetime.strptime(value, "%Y-%m-%d")
except ValueError:
print(f"'{value}' does not match YYYY-MM-DD format")
raise
class Example:
a = DateFormat()
def __init__(self, a) -> None:
self.a = a
ex = Example("2023-111-02")
They’re also often used for logging e.g.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
logging.basicConfig(level=logging.INFO)
class LogThis:
def __set_name__(self, owner, name):
print(f"Instance of {self} created by {owner} assigned to {name}")
self.attrib = name
self._attrib = "_private_" + name
def __get__(self, origin, owner=None):
val = getattr(origin, self._attrib)
logging.info(f"Access of {origin}.{self.attrib}={val}")
return val
def __set__(self, origin, val):
setattr(origin, self._attrib, val)
logging.info(f"Modification of {origin}.{self.attrib}={val}")
class B:
x = LogThis()
def __init__(self, x):
self.x = x
def change_x(self, val):
self.x = val
b = B("test")
b.change_x("another")
b.x
Outputting
1
2
3
4
Instance of <__main__.LogThis object at 0x100b036d0> created by <class '__main__.B'> assigned to x
INFO:root:Modification of <__main__.B object at 0x100bab7d0>.x=test
INFO:root:Modification of <__main__.B object at 0x100bab7d0>.x=another
INFO:root:Access of <__main__.B object at 0x100bab7d0>.x=another
Though I’ve found implementing a decorator for this can be maybe nicer (more convenient?)