Descriptors
Everything defined in a class body are class-attributes (except the docstring). Descriptors are class attributes in Python that manage the attributes in instances. They're essentially reusable properties and serve as a Pythonic way to achieve something similar to setter and getter methods in other programming languages.
An attribute is considered a descriptor when the attribute get, set or delete methods are overwritten.
Basic Example
class Decimal:
def __init__(self, decimals=2):
self.decimals = decimals
def __set_name__(self, owner, name):
self.property_name = f"_{name}"
def __get__(self, obj, objtype=None):
d = f".{self.decimals}f"
return format(getattr(obj, self.property_name), d)
def __set__(self, obj, value):
setattr(obj, self.property_name, value)
def __delete__(self, obj):
# do something
class Coordinate:
x = Decimal(decimals=3)
y = Decimal()
def __init__(self, x, y):
self.x = x
self.y = y
c = Coordinate(x=1, y=2)
print(c.x, c.y) # (1.000, 2.00)
Validation
Validation descriptors can work with __set__ only, and do not require the __get__ method. Store the attribute value directly into __dict__:
instance.__dict__[self.name] = value
Cache
Caching can be done efficiently with __get__ only:
class BookEntry:
def __init__(self):
self.cache = {}
def __get__(self, instance, owner=None):
entry = self.cache.get(id(instance), None)
if entry is not None:
return entry
entry = {
"title": instance.title,
"isbn": instance.isbn,
"author": instance.author,
"year": instance.year
}
entry = json.dumps(entry)
self.cache[id(instance)] = entry
return entry
Lazy Descriptor
class LazyProperty:
def __init__(self, func):
self._func = func
self.__name__ = func.__name__
def __get__(self, obj, cls):
print("function called")
result = self._func(obj)
obj.__dict__[self.__name__] = result
return result
class MyClass:
@LazyProperty
def x(self):
return 1243567
Template
class MyDescriptor:
def __init__(self, field=""):
self.field = field
def __get__(self, obj, owner):
return obj.__dict__.get(self.field)
def __set__(self, obj, val):
obj.__dict__[self.field] = val
def __set_name__(self, owner, name):
self.public_name = name
self.private_name = '_' + name