Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Understanding Python Descriptors: Definition, Types, and Priority

Tech May 10 2

Descriptor Definition

A descriptor is a class that implements at least one of the methods __get__(), __set__(), or __delete__(). Descriptors are used to proxy attributes of another class. It is important to note that descriptors cannot be defined in the constructor of the class that uses them; they must be defined as class attributes. They belong to the class, not to instances. We can verify this by inspecting the dictionaries of both the instance and the class.

Descriptors are the underlying data structure implementation for many Python class features. Commonly used decorators like @classmethod, @staticmethod, @property, and even __slots__ are implemanted via descriptors. They are an important tool for many advanced libraries and frameworks, and a key component in large frameworks that use decorators or metaclasses. (Note: Decorators and metaclasses will be explained in future articles.)

Here is a example of a descriptor and a class that uses it:

class Descriptor:

    def __init__(self, key, value_type):
        self.key = key
        self.value_type = value_type

    def __get__(self, instance, owner):
        print("Executing Descriptor's __get__")
        return instance.__dict__[self.key]

    def __set__(self, instance, value):
        print("Executing Descriptor's __set__")
        if not isinstance(value, self.value_type):
            raise TypeError(f"Parameter {self.key} must be of type {self.value_type}")
        instance.__dict__[self.key] = value

    def __delete__(self, instance):
        print("Executing Descriptor's __delete__")
        instance.__dict__.pop(self.key)


class Person:

    name = Descriptor("name", str)
    age = Descriptor("age", int)

    def __init__(self, name, age):
        self.name = name
        self.age = age


person = Person("xiaoming", 15)
print(person.__dict__)
person.name
person.name = "jone"
print(person.__dict__)

In this example, the class Descriptor is a descriptor, and Person is the class that uses it.

The __dict__ attribute of a class is a built-in property that stores the class's static methods, class methods, regular functions, global variables, and some built-in attributes.

When accessing a descriptor variable, the __get__ method of the descriptor is called. When setting a descriptor variable, the __set__ method is called.

The output of the example above:

Descriptor Example Output

Types and Priority of Descriptors

Descriptors can be classified into two types: data descriptors and non-data descriptors.

  • A data descriptor implements both __set__() and __get__() methods.
  • A non-data descriptor implements only __get__() (or __delete__()) but not __set__().

The priority order for attribute lookup in Python class instances is:

  1. Class attributes (highest)
  2. Data descriptors
  3. Instance attributes
  4. Non-data descriptors
  5. __getattr__() if the attribute is not found (lowest)

In the descriptor example in the previous section, the instance attribute of person has a lower priority than the data descriptor Descriptor. Therefore, both assignment and retrieval invoke the descriptor's methods.

The next example demonstrates that class attributes have higher priority than data descriptors:

class Descriptor:
    def __get__(self, instance, owner):
        print("Executing Descriptor's __get__")
    def __set__(self, instance, value):
        print("Executing Descriptor's __set__")
    def __delete__(self, instance):
        print("Executing Descriptor's __delete__")

class University:
    name = Descriptor()
    def __init__(self, name):
        self.name = name

University.name
University.name = "Shenzhen University"
print(University.name)

my_university = University("Xiamen University")
my_university.name
print(my_university.name)

In this example, the University class attribute name is assigned a value. Because class attributes have the highest priority, that value is always used. The output is:

Class attribute priority output

What would the output be if we delete the line University.name = "Shenzhen University"?

Output after removing class attribute assignment

Why is the instance's name attribute None? Because the instance attribute has lower priority than the descriptor, the descriptor's __get__ is called. Since the descriptor has no stored value, it returns None. Therefore, even though the instance has its own attribute, it cannot be retrieved proper because the descriptor intercepts the access.

The priority order of descriptors can be challenging to understand. It is recommended to write small test examples and observe the output to grasp the concept.

Reference: Original article

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.