描述符(descriptor)是指实现了以下描述符协议中至少一个方法的类:

  • __get__(self, instance, owner):访问属性时触发
  • __set__(self, instance, value):设置属性时触发
  • __delete__(self, instance):删除属性时触发

在 Python 中,描述符(Descriptor) 是一种底层机制,用于控制属性的访问方式。它是通过实现特定的魔法方法(__get__、__set__ 和 __delete__)来实现的。例如:

class MyDescriptor:
    def __get__(self, instance, owner):
        print("调用 __get__")
        return instance._value

    def __set__(self, instance, value):
        print("调用 __set__")
        instance._value = value

class MyClass:
    attr = MyDescriptor()

obj = MyClass()
obj.attr = 42      # 调用 __set__
print(obj.attr)    # 调用 __get__输出 42

在通过类对象或者实例对象读取属性attr时,python调用了这个MyDescriptor()实例的__get__方法,给它3个实参值,其中第一个实参值是描述符对象本身,第二个实参值是MyClass实例对象(可以为None),第三个实参值是MyClass类对象。

描述符的应用场景:

  • 实现@staticmethod、@classmethod、@property等内置装饰器语法糖。甚至是__slots__等的实现。
  • 属性验证(如类型检查、范围限制)
  • 惰性加载属性
  • 缓存机制
  • ORM 框架中的字段定义(如 Django 的 models.Field)

使用描述符,可以让程序员在引用一个对象属性时自定义要完成的工作。本质上看,描述符就是一个类,只不过它定义了另一个类中属性的访问方式。换句话说,一个类可以将属性管理全权委托给描述符类。

如下示例一个描述符及引用描述符类的代码。Descriptors类就是一个描述符,Person是使用描述符的类:

class Descriptors:
 
    def __init__(self, key, value_type):
        self.key = key
        self.value_type = value_type
 
    def __get__(self, instance, owner):
        print("执行Descriptors的get")
        return instance.__dict__[self.key]
 
    def __set__(self, instance, value):
        print("执行Descriptors的set")
        if not isinstance(value, self.value_type):
            raise TypeError("参数%s必须为%s"%(self.key, self.value_type))
        instance.__dict__[self.key] = value 
 
    def __delete__(self, instance):
        print("执行Descriptors的delete")
        instance.__dict__.pop(self.key) 
 
class Person:
 
    name = Descriptors("name", str)
    age = Descriptors("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__)
#输出:
#执行Descriptors的set
#执行Descriptors的set
#{'name': 'xiaoming', 'age': 15}
#执行Descriptors的get
#执行Descriptors的set
#{'name': 'jone', 'age': 15}
  • 至少实现了内置__set__()和__get__()方法的描述符称为数据描述符
  • 实现了除__set__()以外的方法的描述符称为非数据描述符

@property 修饰的属性、数据描述符或非数据描述符定义的属性,都是在类体的顶层定义,而不是在 __init__ 等实例方法中赋值,所以它们都被保存在类对象的 __dict__ 中。

通过类对象或者属性对象使用一个属性时,查找这个属性的操作优先级从高到低顺序:

  • 数据描述符
  • 实例属性:显然在实例对象的__dict__中
  • 非数据描述符
  • 类属性
  • 找不到的属性触发__getattr__()

所以,用className.VarName=value,则为类属性;用instanceName.VarName=value,则优先是数据描述符。

在每次查找obj.attr 属性时,都用类对象的特殊方法 __getattribute__(),调用obj.__getattribute__('attr'):

  1. 先在 obj 的类及其父类的 __dict__ 中查找名为 attr 的属性。如果找到,并且它是一个数据描述符(即定义了 __get__ 和 __set__),则调用该描述符的 __get__ 方法,直接返回。
  2. 如果没有数据描述符,则查找实例对象自身的 __dict__(即 obj.__dict__)。如果找到了,就直接返回实例属性的值。
  3. 如果实例属性没有找到,再查找类及其父类的 __dict__。如果找到,并且它是一个非数据描述符(只定义了 __get__),则调用其 __get__ 方法,返回其值。如果只是普通的类属性(不实现描述符协议),直接返回属性值。
  4. 如果以上都没有找到,则查找类是否定义了 __getattr__ 方法。如果有,则调用 __getattr__(self, attr)。如果没有定义 __getattr__,则抛出 AttributeError。

当执行 obj.attr = value,Python 实际调用 obj.__setattr__('attr', value)。在 object的__setattr__ 的默认实现:

  1. 先检查 obj 的类(及其父类)中有没有名为 attr 的描述符。如果有,并且它实现了 __set__(即是数据描述符),就调用 该描述符.__set__(obj, value)。
  2. 如果没有数据描述符,就把值放进 obj.__dict__。

类对象赋值的默认行为就是直接把属性写入类对象自身的 __dict__中,不会触发数据描述符的 __set__ 方法。描述符的 __set__ 只会在实例赋值(instance.attr = value)时被触发。