descriptors
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.
This is similar to using a @property
decorator; descriptors are re-usable properties.
For example, consider a custom Decimal class that defines custom get
and `set methods for the x and y attributes.
The Decimal class simply returns the value in decimal:
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 f'{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)
Unfortunately, many examples and tutorials online about Python descriptors are incorrect. Like this one:
class Age:
def __init__(self, age = 16):
self.age = age
def __set__(self, obj, age):
if not 10 <= age <= 20:
raise ValueError('Valid age must be in [10, 20]')
self.age = age
def __get__(self, obj, type = None):
return self.age
class Student:
age = Age()
def __init__(self, name, age):
self.name = name
self.age = age
print(f'Student Name: {self.name}, Age:{self.age}')
s1 = Student("John", 13)
s2 = Student("Mike", 15)
It may be hard to spot what is wrong the code, but that happens when we check the instances?
print(s1.age, s1.name) # 15 John
print(s2.age, s2.name) # 15 Mike
Creating s2
overwrites the value of s1
because it is a class attribute and not an instance attribute.
Validation¶
Validation descriptors can work with __set__
only, and do not require the __get__
method.
We should store the attribute value directly into __dict__
, because calling setattr(instance, self.name)
could trigger the __set__
method again, leading to infinite recursion if the name was defined in __init__
instance.__dict__[self.name] = value
Cache¶
Caching can be done efficiently with __get__
only.
The idea is to simply store the output of __get__
for later use.
class Book:
bookentry = BookEntry()
def __init__(self, isbn, title, author, year):
self.isbn = isbn
self.title = title
self.author = author
self.year = year
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 = self.cache.get(id(instance), None)
if entry is not None:
return entry
entry = {"bookentry": instance.title,
"isbn": instance.isbn,
"author": instance.author,
"year": instance.year
}
entry = json.dumps(entry)
self.cache[id(instance)] = entry
return entry
Lazy descriptor¶
The following returns the value directly when it is called a second time. The reason why this works is that the name is not found in the object dict, so it looks in the descriptor. The second time is hits the object dict directly.
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): # owner is the class to which instance belongs
return obj.__dict__.get(self.field)
def __set__(self, obj, val): # self is descriptor instance.
obj.__dict__[self.field] = val
def __set_name__(self, owner, name):
self.public_name = name
self.private_name = '_' + name
More about descriptors: https://docs.python.org/3/howto/descriptor.html Encapsulation with descriptors https://www.youtube.com/watch?v=5GG4jBxj4Ys