跳转至

Python 描述符是什么?

🧠 描述符,一句话:一个能“挟持”另一个类属性访问的对象。它让你能把 obj.attr 这个最简单的操作,变成一段自定义逻辑。


🎭 背后协议:get / set / delete

任何定义了 get 方法的类,它的实例就可以作为另一个类的类属性。当访问这个属性时,Python 会调用 get;赋值时调用 set;删除时调用 delete

image.png

  • 非数据描述符:只定义了 get。实例字典里的同名属性会遮盖它。

  • 数据描述符:同时定义了 getset(或 delete)。优先级最高,实例字典里的同名属性都打不过它。


🛠️ 一个你每天在用却可能没意识到的例子

class Person:
    def __init__(self, name):
        self._name = name

    @property
    def name(self):
        return self._name

    @name.setter
    def name(self, value):
        if not isinstance(value, str):
            raise TypeError("名字必须是字符串")
        self._name = value

这里的 @property 实际上创建了一个描述符对象 namep.name 会调用描述符的 getp.name = 'Tom' 会调用 set。整个 property 机制就是建立在描述符协议上的。


🧩 手写一个描述符:类型检查器

class TypedField:
    def __init__(self, field_type):
        self.field_type = field_type
        self.data = {}          # 用一个字典存不同实例的值

    def __get__(self, obj, objtype=None):
        if obj is None:         # 类访问返回描述符本身
            return self
        return self.data.get(id(obj))

    def __set__(self, obj, value):
        if not isinstance(value, self.field_type):
            raise TypeError(f"需要 {self.field_type}, 收到 {type(value)}")
        self.data[id(obj)] = value

    def __delete__(self, obj):
        del self.data[id(obj)]

class Person:
    name = TypedField(str)
    age = TypedField(int)

这样,属性检查逻辑被复用且隔离在描述符里,业务类干干净净。


🔍 与普通属性的对比

特性 普通属性 描述符
存储位置 实例 dict 描述符自己管理(如内部字典)
访问逻辑 直接取值 触发 get 方法
复用性 每个类要单独写 绑定到类型,一次编写到处复用
典型应用 简单数据 ORM 字段、校验、缓存、惰性加载

🌟 什么时候必须用描述符?

  • 需要在多个属性上复用同一段逻辑,比如类型检查、范围校验。

  • 想要完全掌控属性存取,比如 Django 的 CharField,当获取一个字段时,返回的实际上是一个描述符,背后可能连带着 SQL 构建。

  • 实现单例、缓存、延迟计算,比如懒加载属性:第一次访问时计算,之后缓存结果。

其实 Python 里处处是描述符:@classmethod@staticmethodslots 的一部分实现,都踩在描述符协议上。下次看到 @property,不用记什么魔术,就记着这俩字:描述符。