Python Descriptors Example

Introduction to Python descriptors and an example of how to use Python descriptors to validate attribute value.

1 What are Python descriptors?

A Python descriptor is a class that defines any (one or more) of the following special methods.

  • __get__(self, instance, cls)
  • __set__(self, instance, value)
  • __delete__(self, instance).

If a Python descriptor with __get__() also defines __set__() and/or __delete__() methods, it is a data descriptor.

Python descriptors with only __get__() method defined are known as non-data descriptors. Python methods are non-data descriptors.

2 Usage of Python descriptors

Python descriptor object instances can be used to represent the attributes of other classes and can be reused conviently.

For example, given a Employee class which has two attributes: age and salary. Now the requirement is the values of ageandsalary`attributes should be positive.

2.1 Use Python property

It is much straightforword to achieve the requirement using Python property.

class Employee:
    @property
    def age(self):
        return self._age

    @age.setter
    def age(self, age):
        if age < 0 or age == 0:
            raise ValueError("Value of age should be positive.")
        else:
            self._age = age

    @property
    def salary(self):
        return self._salary

    @salary.setter
    def salary(self, salary):
        if salary < 0 or salary == 0:
            raise ValueError("Value of salary should be positive.")
        else:
            self._salary = salary


if __name__ == '__main__':
    tom = Employee()
    tom.age = 20
    tom.salary = 1000

    jerry = Employee()
    jerry.age =18
    jerry.salary = 0    # raise a ValueError

Note that the same logic codes of checking positive value appear twice, respectively in age setter and salary setter.

2.2 Use Python descriptor

Alternatively, we can create a descriptor class to do the same job.

# A descriptor class
class PositiveAttr(object):
    def __init__(self, name):
        self.name = name
    def __get__(self, instance, cls):
        return instance.__dict__[self.name]
    def __set__(self, instance, value):
        if value < 0 or value == 0:
            raise ValueError("Value of {} should be positive.".format(self.name))
        else:
            instance.__dict__[self.name] = value

Then use the created PositiveAttr descriptor to define attributes in the Employee class. The PositiveAttr descriptor will ensure the value of attributes are positive.

class Employee:
    age = PositiveAttr('age')
    salary = PositiveAttr('salary')

if __name__ == '__main__':
    tom = Employee()
    tom.age = 20
    tom.salary = 1000

    jerry = Employee()
    jerry.age =18
    jerry.salary = 0    # raise a ValueError

Both age and salary are objects of the PositiveAttr descriptor. They share the common codes to to do value validation. So we can also say Python descriptors are some kind of reusable attributes.

3 Python descriptors example

Here is another example about how to use Python descriptors to validate the attributes.

class Attr(object):
    def __init__(self, name, **kw):
        self.name = name
        self.data_type = kw.pop('data_type', None)
        self.max_value = kw.pop('max_value', None)
        self.min_value = kw.pop('min_value', None)

    def __get__(self, instance, cls):
        return instance.__dict__[self.name]
    
    def __set__(self, instance, value):
        # Validate the data type of the value
        if (self.data_type is not None) and (not isinstance(value, self.data_type)):
            raise TypeError("{} should be type of {}".format(self.name, self.data_type))
        
        # Validate the range of the value
        if (self.max_value is not None) and (value > self.max_value):
            raise ValueError("Allowed maximum value of {} is: {}".format(self.name, self.max_value))
        
        # Validate the range of the value
        if (self.min_value is not None) and (value < self.min_value):
            raise ValueError("Allowed minimum value of {} is: {}".format(self.name, self.min_value))

        instance.__dict__[self.name] = value

    def __delete__(self, instance):
        del instance.__dict__[self.name]

class Employee(object):
    age = Attr('age', data_type=int, min_value=18, max_value=140)
    salary = Attr('salary', data_type=float, min_value=4000.0)


if __name__ == '__main__':
    tom = Employee()
    tom.age = 18
    print(tom.age)
    tom.salary = 5999.86
    print(tom.salary)

    jerry = Employee()
    jerry.age = '18'    # raise TypeError
    jerry.salary = 3500.0   # raise ValueError

In above example, data_type was used to validate the data type of the attribute value; min_value and max_value defined the allowed range of the attribute value.

Comments