I’ve been learning Python for a long time, and when I talk about the elegance of Python, the Descriptor features that I can say off the top of my head can be numbered.

Descriptors are unique to the Python language and are used not only at the application layer but also in the language infrastructure.

I would venture to guess that your understanding of descriptors began with field objects such as Django ORM and SQLAlchemy, and yes, they are descriptors. Your understanding of it probably stops there, and if you don’t look into it, why is it designed the way it is? We also miss the convenience and elegance of Python.

Because descriptors are long and verbose, they tend to make you tired, so I’m going to break them up into several chapters.

Today’s topic is: Why use descriptors?

Imagine you’re writing a grade management system for your school, and if you don’t have much coding experience, you might write something like this.

class Student:
    def __init__(self, name, math, chinese, english):
        self.name = name
        self.math = math
        self.chinese = chinese
        self.english = english

    def __repr__(self):
        return "<Student: {}, math:{}, chinese: {}, english:{}>".format(
                self.name, self.math, self.chinese, self.english
            )
Copy the code

It all seems reasonable

>>> std1 = Student('Ming'.76.87.68)
>>> Std1 <Student: math:76, chinese: 87, english:68>
Copy the code

However, the program is not as intelligent as human beings and will not automatically judge the validity of the data according to the application scenario. If the teacher accidentally records the score as a negative number or more than 100, the program will not be able to sense it.

You’re smart enough to add judgment logic to your code right away.

class Student:
    def __init__(self, name, math, chinese, english):
        self.name = name
        if 0 <= math <= 100:
            self.math = math
        else:
            raise ValueError("Valid value must be in [0, 100]")
        
        if 0 <= chinese <= 100:
            self.chinese = chinese
        else:
            raise ValueError("Valid value must be in [0, 100]")
      
        if 0 <= chinese <= 100:
            self.english = english
        else:
            raise ValueError("Valid value must be in [0, 100]")
        

    def __repr__(self):
        return "<Student: {}, math:{}, chinese: {}, english:{}>".format(
                self.name, self.math, self.chinese, self.english
            )
Copy the code

Now it’s a little artificial intelligence, able to tell right from wrong on its own.

The program is smart, but there is too much logic in __init__, which affects the readability of the code. It just so happens that you’ve learned the Property Property, and it’s a good place to apply it. So you change the code to look like this, and the readability of the code is instantly improved

class Student:
    def __init__(self, name, math, chinese, english):
        self.name = name
        self.math = math
        self.chinese = chinese
        self.english = english

    @property
    def math(self):
        return self._math

    @math.setter
    def math(self, value):
        if 0 <= value <= 100:
            self._math = value
        else:
            raise ValueError("Valid value must be in [0, 100]")

    @property
    def chinese(self):
        return self._chinese

    @chinese.setter
    def chinese(self, value):
        if 0 <= value <= 100:
            self._chinese = value
        else:
            raise ValueError("Valid value must be in [0, 100]")

    @property
    def english(self):
        return self._english

    @english.setter
    def english(self, value):
        if 0 <= value <= 100:
            self._english = value
        else:
            raise ValueError("Valid value must be in [0, 100]")

    def __repr__(self):
        return "<Student: {}, math:{}, chinese: {}, english:{}>".format(
                self.name, self.math, self.chinese, self.english
            )
Copy the code

The program is the same artificial intelligence, very good.

You think the code you’ve written is so good that it’s invulnerable.

Unexpectedly, one day, Xiao Ming looked at your code and sighed deeply: the three attributes in the class, math, Chinese and English, all use Property to effectively control the validity of attributes. Functionally, there is no problem, but it is too wordy, the legitimacy logic of the three variables are the same, as long as greater than 0, less than 100 can be, the code repetition rate is too high, here three scores are ok, but suppose there are geography, biology, history, chemistry and other scores, the code is unbearable. Learn about Python descriptors.

After Xiao Ming’s guidance, you know the thing “descriptor”. In awe, you do some research on the use of descriptors.

A class that implements the descriptor protocol is a descriptor.

What descriptor protocol: A class that implements at least one of __get__(), __set__(), and __delete__() is a descriptor.

  • __get__: Used to access properties. It returns the value of the attribute and throws an exception if the attribute is nonexistent, illegal, and so on.
  • __set__: will be called in the property assignment operation. Nothing is returned.
  • __delete__: Controls the deletion operation. No content is returned.

With a general understanding of the descriptors, you begin to rewrite the above methods.

As mentioned earlier, the Score class is a descriptor that uses three special methods to access math, Chinese, and English from the Student instance. Score here avoids the embarrassment of having to reuse a lot of code using Property.

class Score:
    def __init__(self, default=0):
        self._score = default

    def __set__(self, instance, value):
        if not isinstance(value, int):
            raise TypeError('Score must be integer')
        if not 0 <= value <= 100:
            raise ValueError('Valid value must be in [0, 100]')

        self._score = value

    def __get__(self, instance, owner):
        return self._score

    def __delete__(self):
        del self._score
        
class Student:
    math = Score(0)
    chinese = Score(0)
    english = Score(0)

    def __init__(self, name, math, chinese, english):
        self.name = name
        self.math = math
        self.chinese = chinese
        self.english = english


    def __repr__(self):
        return "<Student: {}, math:{}, chinese: {}, english:{}>".format(
                self.name, self.math, self.chinese, self.english
            )
Copy the code

The result is the same as the previous one, with effective control over the validity of the data (field types, numeric ranges, etc.).

Above, I gave a concrete example, from the original coding style to the Property, and finally to the descriptor. Step by step, take you through the elegance of the descriptor.

The only thing you need to remember from this article is the coding convenience descriptors provide. They protect properties from modification, check property types, and greatly increase code reuse.