Python中的描述符(Descriptor)
字数 2332 2025-11-03 12:22:57

Python中的描述符(Descriptor)

描述符是Python中一个高级但非常重要的概念,它允许你自定义在属性访问时发生的操作。理解描述符是掌握Python面向对象编程深层次机制的关键。

1. 什么是描述符?

简单来说,描述符是一个“绑定行为”的类属性。它不是一个独立的函数,而是一个实现了特定协议(即拥有__get____set____delete__方法中至少一个)的类。

当一个类的属性被定义为描述符实例时,对该属性的访问(读取、赋值、删除)将不再直接操作该实例的属性字典,而是转而触发描述符类中定义的相应方法。

2. 描述符协议

描述符协议由三个特殊方法组成。一个类只要实现了以下任何一个方法,它就被认为是一个描述符:

  • __get__(self, obj, type=None) -> object:当访问描述符属性时调用。
    • obj:访问该属性的实例对象。如果属性是通过类访问的(如MyClass.descriptor_attr),则objNone
    • type:该实例所属的
  • __set__(self, obj, value) -> None:当为描述符属性赋值时调用。
    • obj:实例对象。
    • value:要赋的值。
  • __delete__(self, obj) -> None:当删除描述符属性时调用。
    • obj:实例对象。

根据实现的方法,描述符可分为两类:

  • 数据描述符:实现了__set____delete__方法的描述符。
  • 非数据描述符:只实现了__get__方法的描述符。

这个区分非常重要,因为它影响了Python的属性查找优先级。

3. 属性查找优先级

当您通过实例访问一个属性(如instance.attr)时,Python会按照以下顺序进行查找:

  1. 数据描述符:在类及其父类中查找名为attr数据描述符。如果找到,则调用其__get__方法。
  2. 实例属性:在实例的__dict__字典中查找名为attr的键。
  3. 非数据描述符:在类及其父类中查找名为attr非数据描述符。如果找到,则调用其__get__方法。
  4. 类属性:在类的__dict__字典中查找名为attr的键。
  5. 父类属性:按照方法解析顺序(MRO)在父类中查找。
  6. 如果以上都未找到,则抛出AttributeError

关键点:数据描述符的优先级高于实例属性!这意味着即使实例的__dict__中有同名属性,Python也会优先使用数据描述符。

4. 循序渐进:从简单例子到实际应用

第一步:创建一个非数据描述符

我们先创建一个最简单的描述符,它只实现了__get__方法,因此是一个非数据描述符。

class SimpleDescriptor:
    """一个简单的非数据描述符,记录访问次数"""
    def __init__(self):
        self._value = "默认值"

    def __get__(self, obj, objtype=None):
        print(f"描述符的 __get__ 被调用:obj={obj}, objtype={objtype}")
        return self._value

class MyClass:
    attr = SimpleDescriptor()  # 类属性是一个描述符实例

# 通过类访问
print("1. 通过类访问:")
print(MyClass.attr)  # 输出:描述符的 __get__ 被调用:obj=None, objtype=<class '__main__.MyClass'>
                     #       默认值

# 通过实例访问
print("\n2. 通过实例访问:")
instance = MyClass()
print(instance.attr) # 输出:描述符的 __get__ 被调用:obj=<__main__.MyClass object at ...>, objtype=<class '__main__.MyClass'>
                     #       默认值

# 尝试为实例属性赋值(不会调用描述符的__set__,因为我们没定义)
print("\n3. 为实例属性赋值:")
instance.attr = "实例属性的值" # 这会在实例的 __dict__ 中创建一个名为 'attr' 的属性
print(instance.attr)         # 输出:实例属性的值
print(instance.__dict__)     # 输出:{'attr': '实例属性的值'}
print(MyClass.attr)          # 输出:描述符的 __get__ 被调用:obj=None, objtype=<class '__main__.MyClass'>
                             #       默认值

解释

  • 通过类访问MyClass.attr时,__get__obj参数是None
  • 通过实例访问instance.attr时,__get__obj参数是该实例。
  • 当我们执行instance.attr = "..."时,由于SimpleDescriptor没有__set__方法(是非数据描述符),Python会在实例的__dict__中创建(或覆盖)一个名为attr的普通属性。之后再次访问instance.attr,根据优先级规则,会优先找到实例属性,而不会再触发描述符的__get__

第二步:创建一个数据描述符

现在,我们创建一个完整的数据描述符,用于管理一个属性,并在赋值时进行类型检查。

class TypedDescriptor:
    """一个数据描述符,进行类型检查"""
    def __init__(self, name, expected_type):
        self.name = name           # 属性的名称
        self.expected_type = expected_type # 期望的类型
        self.private_name = f"_{name}"     # 用于在实例中存储值的私有属性名

    def __get__(self, obj, objtype=None):
        if obj is None:
            # 通过类访问时,返回描述符本身
            return self
        # 从实例的 __dict__ 中获取存储的值
        return getattr(obj, self.private_name)

    def __set__(self, obj, value):
        if not isinstance(value, self.expected_type):
            raise TypeError(f"期望的类型是 {self.expected_type},但传入的是 {type(value)}")
        # 将值存储在实例的 __dict__ 中,使用私有名称以避免冲突
        setattr(obj, self.private_name, value)

    def __delete__(self, obj):
        # 删除存储在实例中的值
        delattr(obj, self.private_name)

class Person:
    name = TypedDescriptor("name", str)   # 类属性,是描述符实例
    age = TypedDescriptor("age", int)     # 另一个描述符实例

    def __init__(self, name, age):
        self.name = name  # 这会调用 TypedDescriptor.__set__
        self.age = age    # 这会调用 TypedDescriptor.__set__

# 正常使用
print("1. 正常创建对象:")
person = Person("Alice", 30)
print(person.name)  # 输出:Alice (调用 __get__)
print(person.age)   # 输出:30 (调用 __get__)

# 类型检查生效
print("\n2. 类型错误测试:")
try:
    person.age = "三十" # 不是整数,会抛出 TypeError
except TypeError as e:
    print(e) # 输出:期望的类型是 <class 'int'>,但传入的是 <class 'str'>

# 数据描述符优先级测试
print("\n3. 数据描述符优先级测试:")
person.__dict__["age"] = "偷偷设置的字符串" # 直接操作实例字典
print(person.age) # 输出:30 (仍然是30!)

解释

  • 现在TypedDescriptor是一个数据描述符(实现了__set__)。
  • __set__中,我们进行了类型检查。
  • 我们使用了一个“私有”名称(如_age)在实例的__dict__中存储实际数据,以避免与描述符本身同名造成无限递归。
  • 最关键的部分是最后的测试:即使我们直接向实例的__dict__中写入了一个age属性,当我们访问person.age时,Python仍然优先调用了数据描述符的__get__方法,返回了存储在_age中的正确值30。这证明了数据描述符的优先级高于实例属性。

第三步:实际应用场景

描述符在Python中广泛应用,以下是一些经典例子:

  1. 属性(@property:Python内置的@property装饰器本质上就是创建了一个数据描述符。@property是描述符的一种优雅、简洁的语法糖。
  2. 方法(methods:类中的普通函数也是非数据描述符。当你调用instance.method()时,Python会通过描述符协议将函数绑定到实例上,生成一个绑定方法。
  3. ORM(对象关系映射)框架:例如Django的模型字段(models.CharFieldmodels.IntegerField)都是描述符。它们负责在Python对象和数据库列值之间进行转换和验证。
  4. 惰性求值:描述符可以用于实现惰性属性,即只有在第一次访问时才计算其值,然后缓存结果。

总结

  • 描述符是一个实现了__get____set____delete__方法的类。
  • 数据描述符(有__set__/__delete__)优先级高于实例属性;非数据描述符(只有__get__)优先级低于实例属性。
  • 描述符的核心作用是将类属性的访问逻辑封装到一个单独的类中,从而实现复用、验证、惰性计算等高级功能。
  • 理解描述符协议和属性查找顺序是掌握描述符的关键。
Python中的描述符(Descriptor) 描述符是Python中一个高级但非常重要的概念,它允许你自定义在属性访问时发生的操作。理解描述符是掌握Python面向对象编程深层次机制的关键。 1. 什么是描述符? 简单来说,描述符是一个“绑定行为”的类属性。它不是一个独立的函数,而是一个实现了特定协议(即拥有 __get__ 、 __set__ 或 __delete__ 方法中至少一个)的类。 当一个类的属性被定义为描述符实例时,对该属性的访问(读取、赋值、删除)将不再直接操作该实例的属性字典,而是转而触发描述符类中定义的相应方法。 2. 描述符协议 描述符协议由三个特殊方法组成。一个类只要实现了以下任何一个方法,它就被认为是一个描述符: __get__(self, obj, type=None) -> object :当访问描述符属性时调用。 obj :访问该属性的 实例对象 。如果属性是通过类访问的(如 MyClass.descriptor_attr ),则 obj 为 None 。 type :该实例所属的 类 。 __set__(self, obj, value) -> None :当为描述符属性赋值时调用。 obj :实例对象。 value :要赋的值。 __delete__(self, obj) -> None :当删除描述符属性时调用。 obj :实例对象。 根据实现的方法,描述符可分为两类: 数据描述符 :实现了 __set__ 或 __delete__ 方法的描述符。 非数据描述符 :只实现了 __get__ 方法的描述符。 这个区分非常重要,因为它影响了Python的属性查找优先级。 3. 属性查找优先级 当您通过实例访问一个属性(如 instance.attr )时,Python会按照以下顺序进行查找: 数据描述符 :在类及其父类中查找名为 attr 的 数据描述符 。如果找到,则调用其 __get__ 方法。 实例属性 :在实例的 __dict__ 字典中查找名为 attr 的键。 非数据描述符 :在类及其父类中查找名为 attr 的 非数据描述符 。如果找到,则调用其 __get__ 方法。 类属性 :在类的 __dict__ 字典中查找名为 attr 的键。 父类属性 :按照方法解析顺序(MRO)在父类中查找。 如果以上都未找到,则抛出 AttributeError 。 关键点 :数据描述符的优先级高于实例属性!这意味着即使实例的 __dict__ 中有同名属性,Python也会优先使用数据描述符。 4. 循序渐进:从简单例子到实际应用 第一步:创建一个非数据描述符 我们先创建一个最简单的描述符,它只实现了 __get__ 方法,因此是一个非数据描述符。 解释 : 通过类访问 MyClass.attr 时, __get__ 的 obj 参数是 None 。 通过实例访问 instance.attr 时, __get__ 的 obj 参数是该实例。 当我们执行 instance.attr = "..." 时,由于 SimpleDescriptor 没有 __set__ 方法(是非数据描述符),Python会在实例的 __dict__ 中创建(或覆盖)一个名为 attr 的普通属性。之后再次访问 instance.attr ,根据优先级规则,会优先找到实例属性,而不会再触发描述符的 __get__ 。 第二步:创建一个数据描述符 现在,我们创建一个完整的数据描述符,用于管理一个属性,并在赋值时进行类型检查。 解释 : 现在 TypedDescriptor 是一个 数据描述符 (实现了 __set__ )。 在 __set__ 中,我们进行了类型检查。 我们使用了一个“私有”名称(如 _age )在实例的 __dict__ 中存储实际数据,以避免与描述符本身同名造成无限递归。 最关键的部分是最后的测试:即使我们直接向实例的 __dict__ 中写入了一个 age 属性,当我们访问 person.age 时,Python仍然优先调用了数据描述符的 __get__ 方法,返回了存储在 _age 中的正确值 30 。这证明了数据描述符的优先级高于实例属性。 第三步:实际应用场景 描述符在Python中广泛应用,以下是一些经典例子: 属性( @property ) :Python内置的 @property 装饰器本质上就是创建了一个数据描述符。 @property 是描述符的一种优雅、简洁的语法糖。 方法( methods ) :类中的普通函数也是非数据描述符。当你调用 instance.method() 时,Python会通过描述符协议将函数绑定到实例上,生成一个绑定方法。 ORM(对象关系映射)框架 :例如Django的模型字段( models.CharField , models.IntegerField )都是描述符。它们负责在Python对象和数据库列值之间进行转换和验证。 惰性求值 :描述符可以用于实现惰性属性,即只有在第一次访问时才计算其值,然后缓存结果。 总结 描述符 是一个实现了 __get__ 、 __set__ 或 __delete__ 方法的类。 数据描述符 (有 __set__/__delete__ )优先级高于实例属性; 非数据描述符 (只有 __get__ )优先级低于实例属性。 描述符的核心作用是 将类属性的访问逻辑封装到一个单独的类中 ,从而实现复用、验证、惰性计算等高级功能。 理解描述符协议和属性查找顺序是掌握描述符的关键。