Python 描述符是什么?
🧠 描述符,一句话:一个能“挟持”另一个类属性访问的对象。它让你能把 obj.attr 这个最简单的操作,变成一段自定义逻辑。
🎭 背后协议:get / set / delete¶
任何定义了 get 方法的类,它的实例就可以作为另一个类的类属性。当访问这个属性时,Python 会调用 get;赋值时调用 set;删除时调用 delete。

-
非数据描述符:只定义了
get。实例字典里的同名属性会遮盖它。 -
数据描述符:同时定义了
get和set(或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 实际上创建了一个描述符对象 name。p.name 会调用描述符的 get,p.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、@staticmethod、slots 的一部分实现,都踩在描述符协议上。下次看到 @property,不用记什么魔术,就记着这俩字:描述符。