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