I think a good reference in the subject available to everyone, is actually the official docs in this Descriptors How To
I setup an example, but note that there is a lot more about descriptors, and you shouldn't be using this unless writing a framework or some library (like an ORM) that requires the dynamic instantiation and validation of fields of different types for example.
For the usual validation needs, limit yourself to property decorator.
class PositionX: # from 0 to 1000
def __init__(self, x):
self.x = x
print('***Start***')
print()
print('Original PositionX class')
pos1 = PositionX(50)
print(pos1.x)
pos1.x = 100
print(pos1.x)
pos1.x = -10
print(pos1.x)
print()
# let's validate x with a property descriptor, using @property
class PositionX: # from 0 to 1000
def __init__(self, position):
self._x = position
@property
def x(self):
return self._x
@x.setter
def x(self, value):
if 0 <= value <= 1000:
self._x = value
else:
raise ValueError
print('PositionX attribute x validated with @property')
pos2 = PositionX(50)
print(pos2.x)
pos2.x = 100
print(pos2.x)
try:
pos2.x = -10
except ValueError:
print("Can't set x to -10")
print()
# Let's try instead to use __set__ and __get__ in the original class
# This is wrong and won't work. This makes the class PositionX a descriptor,
# while we wanted to protect x attribute of PositionX with the descriptor.
class PositionX: # from 0 to 1000
def __init__(self, x):
self.x = x
def __get__(self, instance):
print('Running __get__')
return self._x
def __set__(self, instance, value):
print('Running __set__')
if 0 <= value <= 1000:
self._x = value
else:
raise ValueError
print("Using __set__ and __get__ in the original class. Doesn't work.")
print("__get__ and __set__ don't even run because x is found in the pos3 instance and there is no descriptor object by the same name in the class.")
pos3 = PositionX(50)
print(pos3.x)
pos3.x = 100
print(pos3.x)
try:
pos3.x = -10
except ValueError:
print("Can't set x to -10")
print(pos3.x)
print()
# Let's define __set__ and __get__ to validate properties like x
# (with the range 0 to 1000). This actually makes the class Range0to1000
# a data descriptor. The instance dictionary of the managed class PositionX
# is always overrided by the descriptor.
# This works because now on x attribute reads and writes of a PositionX
# instance the __get__ or __set__ descriptor methods are always run.
# When run they get or set the PositionX instance __dict__ to bypass the
# trigger of descriptor __get__ or __set__ (again)
class Range0to1000:
def __init__(self, name): # the property name, 'x', 'y', whatever
self.name = name
self.value = None
def __get__(self, instance, managed_class):
print('Running __get__')
return instance.__dict__[self.name]
# same as getattr(instance, self.name) but doesn't trigger
# another call to __get__ leading to recursion error
def __set__(self, instance, value):
print('Running __set__')
if 0 <= value <= 1000:
instance.__dict__[self.name] = value
# same as setattr(instance, self.name, self.value) but doesn't
# trigger another call to __set__ leading to recursion error
else:
raise ValueError
class PositionX: # holds a x attribute from 0 to 1000
x = Range0to1000('x') # no easy way to avoid passing the name string 'x'
# but now you can add many other properties
# sharing the same validation code
# y = Range0to1000('y')
# ...
def __init__(self, x):
self.x = x
print("Using a descriptor class to validate x.")
pos4 = PositionX(50)
print(pos4.x)
pos4.x = 100
print(pos4.x)
try:
pos4.x = -10
except ValueError:
print("Can't set x to -10")
print(pos4.x)
print()
print('***End***')