← 返回题目列表

Python 描述符是什么?它和 property 有什么关系?

高频 困难 第 14 / 21 题 更新于 2026/07/27
描述符property__get____set__属性访问

简化版

描述符是实现了 __get____set____delete__ 中任意方法的对象,用来接管属性访问逻辑。property 本质上就是一个描述符,它通过描述符协议把方法包装成属性访问。

详细版

描述符示例:

class Positive:
    def __set_name__(self, owner, name):
        self.name = "_" + name

    def __get__(self, instance, owner):
        if instance is None:
            return self
        return getattr(instance, self.name)

    def __set__(self, instance, value):
        if value <= 0:
            raise ValueError("value must be positive")
        setattr(instance, self.name, value)

class Product:
    price = Positive()

    def __init__(self, price):
        self.price = price

调用:

p = Product(10)
p.price = -1  # ValueError

描述符常用于 ORM 字段、表单校验、属性计算、懒加载、方法绑定等场景。

描述符分两类:

  • 数据描述符:实现了 __set____delete__
  • 非数据描述符:只实现 __get__

数据描述符优先级高于实例属性;非数据描述符可能被实例属性遮蔽。

完整版教学

一、属性访问不只是查字典

当你写:

obj.x

Python 并不是简单地去 obj.__dict__ 里取 x。如果类属性 x 是描述符,Python 会触发描述符协议。

描述符是一个对象,只要它实现了下面任意方法:

__get__(self, instance, owner)
__set__(self, instance, value)
__delete__(self, instance)

就可以参与属性访问。

二、property 为什么是描述符

看普通 property:

class Circle:
    def __init__(self, radius):
        self._radius = radius

    @property
    def radius(self):
        return self._radius

    @radius.setter
    def radius(self, value):
        if value <= 0:
            raise ValueError("radius must be positive")
        self._radius = value

访问:

c.radius

看起来像读属性,背后会调用 property 对象的 __get__;赋值:

c.radius = 10

背后会调用 property 对象的 __set__

所以 property 不是语法糖那么简单,它是标准库内置的描述符应用。

三、自己实现一个描述符

比如做正整数校验:

class Positive:
    def __set_name__(self, owner, name):
        self.private_name = "_" + name

    def __get__(self, instance, owner):
        if instance is None:
            return self
        return getattr(instance, self.private_name)

    def __set__(self, instance, value):
        if value <= 0:
            raise ValueError("must be positive")
        setattr(instance, self.private_name, value)

使用:

class Order:
    amount = Positive()

    def __init__(self, amount):
        self.amount = amount

__set_name__ 会在类创建时告诉描述符:你被绑定到了哪个类的哪个属性名上。

四、数据描述符和非数据描述符

数据描述符实现了 __set____delete__

class DataDescriptor:
    def __get__(self, instance, owner):
        ...
    def __set__(self, instance, value):
        ...

非数据描述符只实现 __get__

class NonDataDescriptor:
    def __get__(self, instance, owner):
        ...

差异在属性查找优先级上:

  • 数据描述符优先于实例字典;
  • 实例字典优先于非数据描述符。

这也是为什么 property 带 setter 时很难被实例属性直接绕过。

五、方法绑定也和描述符有关

类里的函数对象也是描述符。

class User:
    def hello(self):
        return "hello"

u = User()
u.hello()

当访问 u.hello 时,函数的 __get__ 会把函数和实例绑定起来,形成绑定方法。于是调用时 Python 会自动传入 self

所以描述符是 Python 对象模型的底层机制之一,不只是 property 的实现细节。

六、描述符适合哪些场景

描述符适合把一类属性访问规则复用起来。

例如:

  • ORM 字段映射;
  • 字段类型校验;
  • 懒加载属性;
  • 只读属性;
  • 缓存属性;
  • 框架级声明式 API。

如果只是一个类的一个简单属性,用 property 更直接;如果同样规则要复用到很多字段,描述符更合适。

七、常见误区与追问

记忆钩子:描述符就是“属性访问拦截器”。只要类属性对象实现了 __get____set____delete__ 中的相关方法,访问实例属性时就可能先进入它。

  • 误区:描述符只和 property 有关。 property 是描述符的典型应用,但普通函数变成绑定方法、staticmethodclassmethod、ORM 字段也都依赖描述符机制。
  • 追问:数据描述符和非数据描述符谁优先级更高? 数据描述符实现 __set____delete__,优先级高于实例字典;非数据描述符通常会被实例同名属性遮蔽。
  • 误区:描述符实例会为每个对象单独创建。 描述符通常作为类属性存在,一个描述符对象被这个类的多个实例共享,所以真实数据常放到实例 __dict__ 或其他按实例隔离的存储里。
  • 追问:__get__(self, instance, owner)instance 什么时候是 None 通过类访问属性时,例如 User.nameinstanceNone;描述符可以在这种情况下返回自身,方便调试或框架收集字段定义。
  • 误区:有了描述符就应该替代所有 getter/setter。 描述符适合复用属性管理逻辑,例如校验、转换、延迟加载;单个简单属性用 property 往往更轻。
  • 追问:为什么方法调用会自动传入 self? 函数对象实现了描述符协议,通过实例访问函数时会返回绑定方法,把实例绑定为第一个参数;所以 obj.method() 才等价于 Class.method(obj)

八、加强记忆

描述符就是“能接管属性读写删除的对象”,核心方法是 __get____set____delete__property 是描述符的内置应用,方法绑定也依赖描述符。面试时讲到这里,Python OOP 的底层味道就出来了。