前置知识: Python

描述符协议

00:00
4 min Advanced 2026/6/14

描述符协议与属性管理

什么是描述符

描述符是 Python 中一种强大的协议,它允许你自定义属性的访问行为。当你用 obj.attr 访问属性时,如果这个属性是一个描述符对象,Python 就会调用描述符的方法,而不是直接返回属性值。

描述符是 Python 许多底层机制的实现基础。property、classmethod、staticmethod 都是通过描述符实现的。理解描述符能让你写出更优雅、更可复用的属性管理代码。

基础概念

描述符协议

一个描述符是实现以下任意方法的类:

  • __get__(self, obj, objtype):当属性被读取时调用
  • __set__(self, obj, value):当属性被赋值时调用
  • __delete__(self, obj):当属性被删除时调用

数据描述符与非数据描述符

  • 数据描述符:同时实现了 __get__ 和 __set__(或 __delete__)的描述符。它的优先级高于实例属性
  • 非数据描述符:只实现了 __get__ 的描述符。它的优先级低于实例属性

这个区别很重要,它决定了当实例属性描述符同名时,哪个优先。

查找顺序

当你访问 obj.attr 时,Python 的查找顺序是:

  1. 数据描述符(在定义的)
  2. 实例属性(在 __dict__ 中)
  3. 非数据描述符(在定义的)
  4. 类属性

快速上手

最简单的描述符

class SimpleDescriptor:
    """最简单的描述符:记录属性被访问的次数"""

    def __init__(self, name):
        self.name = name
        self.access_count = 0

    def __get__(self, obj, objtype=None):
        self.access_count += 1
        print(f"{self.name} 被访问了 {self.access_count} 次")
        # 从实例的 __dict__ 中获取实际值
        return obj.__dict__.get(self.name)

    def __set__(self, obj, value):
        print(f"{self.name} 被设置为 {value}")
        obj.__dict__[self.name] = value

class MyClass:
    # 使用描述符
    name = SimpleDescriptor("name")

obj = MyClass()
obj.name = "张三"       # 输出: name 被设置为 张三
print(obj.name)          # 输出: name 被访问了 1 次, 然后输出: 张三
obj.name = "李四"       # 输出: name 被设置为 李四
print(obj.name)          # 输出: name 被访问了 2 次, 然后输出: 李四

使用 property(内置描述符)

property 是 Python 内置的描述符工厂,是最常用的描述符形式:

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

    @property
    def name(self):
        """获取名字"""
        return self._name

    @name.setter
    def name(self, value):
        """设置名字,自动去除首尾空格"""
        if not value:
            raise ValueError("名字不能为空")
        self._name = value.strip()

    @name.deleter
    def name(self):
        """删除名字"""
        print("名字已被删除")
        del self._name

# 使用
p = Person("  张三  ")
print(p.name)      # 张三(自动去除了空格)
p.name = "  李四  "
print(p.name)      # 李四
del p.name         # 名字已被删除

详细用法

类型验证描述符

创建一个可复用的类型验证描述符

class ValidatedAttribute:
    """类型验证描述符:确保属性值的类型正确"""

    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
        return getattr(obj, self.private_name, None)

    def __set__(self, obj, value):
        if not isinstance(value, self.expected_type):
            raise TypeError(
                f"{self.name} 应该是 {self.expected_type.__name__} 类型,"
                f"但传入了 {type(value).__name__}"
            )
        setattr(obj, self.private_name, value)

class User:
    name = ValidatedAttribute('name', str)
    age = ValidatedAttribute('age', int)

# 使用
user = User()
user.name = "张三"     # 正确
user.age = 25          # 正确
# user.age = "25"     # TypeError: age 应该是 int 类型,但传入了 str

范围验证描述符

class RangeValidated:
    """范围验证描述符:确保数值在指定范围内"""

    def __init__(self, name, min_value=None, max_value=None):
        self.name = name
        self.min_value = min_value
        self.max_value = max_value
        self.private_name = f'_{name}'

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        return getattr(obj, self.private_name, None)

    def __set__(self, obj, value):
        if self.min_value is not None and value < self.min_value:
            raise ValueError(f"{self.name} 不能小于 {self.min_value}")
        if self.max_value is not None and value > self.max_value:
            raise ValueError(f"{self.name} 不能大于 {self.max_value}")
        setattr(obj, self.private_name, value)

class Student:
    age = RangeValidated('age', min_value=0, max_value=150)
    score = RangeValidated('score', min_value=0, max_value=100)

student = Student()
student.age = 20       # 正确
student.score = 95     # 正确
# student.age = -1    # ValueError: age 不能小于 0
# student.score = 150 # ValueError: score 不能大于 100

延迟计算描述符

class LazyProperty:
    """延迟计算描述符:第一次访问时计算,之后缓存结果"""

    def __init__(self, func):
        self.func = func
        self.name = func.__name__

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        # 计算并缓存结果
        value = self.func(obj)
        obj.__dict__[self.name] = value
        return value

class DataProcessor:
    def __init__(self, data):
        self.data = data

    @LazyProperty
    def expensive_result(self):
        """耗时的计算,只在第一次访问时执行"""
        print("执行耗时计算...")
        import time
        time.sleep(1)
        return sum(x ** 2 for x in self.data)

processor = DataProcessor(range(1000))
print("第一次访问:")
print(processor.expensive_result)  # 会执行计算
print("第二次访问:")
print(processor.expensive_result)  # 直接返回缓存结果

只读属性描述符

class ReadOnly:
    """只读属性描述符:只能在初始化时设置,之后不可修改"""

    def __init__(self, name):
        self.name = name
        self.private_name = f'_{name}'

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        return getattr(obj, self.private_name, None)

    def __set__(self, obj, value):
        # 如果已经有值,则不允许修改
        if hasattr(obj, self.private_name):
            raise AttributeError(f"{self.name} 是只读属性")
        setattr(obj, self.private_name, value)

class Config:
    api_key = ReadOnly('api_key')
    base_url = ReadOnly('base_url')

    def __init__(self, api_key, base_url):
        self.api_key = api_key    # 初始化时可以设置
        self.base_url = base_url

config = Config("secret-key", "https://api.example.com")
# config.api_key = "new-key"  # AttributeError: api_key 是只读属性

常见场景

实现类似 Django ORM 的字段定义

class Field:
    """数据库字段描述符基类"""
    def __init__(self, name=None, nullable=True, default=None):
        self.name = name
        self.nullable = nullable
        self.default = default
        self.private_name = None

    def __set_name__(self, owner, name):
        """Python 3.6+ 自动调用,获取字段名"""
        self.name = name
        self.private_name = f'_{name}'

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        return getattr(obj, self.private_name, self.default)

    def __set__(self, obj, value):
        self.validate(value)
        setattr(obj, self.private_name, value)

    def validate(self, value):
        if value is None and not self.nullable:
            raise ValueError(f"{self.name} 不能为空")

class CharField(Field):
    def __init__(self, max_length=255, **kwargs):
        super().__init__(**kwargs)
        self.max_length = max_length

    def validate(self, value):
        super().validate(value)
        if value is not None and len(str(value)) > self.max_length:
            raise ValueError(f"{self.name} 长度不能超过 {self.max_length}")

class IntegerField(Field):
    def validate(self, value):
        super().validate(value)
        if value is not None and not isinstance(value, int):
            raise TypeError(f"{self.name} 必须是整数")

class User:
    name = CharField(max_length=100, nullable=False)
    age = IntegerField(nullable=True, default=0)

user = User()
user.name = "张三"
user.age = 25
# user.name = "A" * 200  # ValueError: name 长度不能超过 100

实现观察者模式

class ObservableAttribute:
    """可观察属性:值变化时通知观察者"""

    def __init__(self, name):
        self.name = name
        self.private_name = f'_{name}'

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        return getattr(obj, self.private_name, None)

    def __set__(self, obj, value):
        old_value = getattr(obj, self.private_name, None)
        setattr(obj, self.private_name, value)
        if old_value != value and hasattr(obj, '_observers'):
            for callback in obj._observers:
                callback(self.name, old_value, value)

class Model:
    status = ObservableAttribute('status')

    def __init__(self):
        self._observers = []

    def observe(self, callback):
        self._observers.append(callback)

def on_status_change(attr, old, new):
    print(f"状态变化: {old} -> {new}")

model = Model()
model.observe(on_status_change)
model.status = "running"   # 输出: 状态变化: None -> running
model.status = "stopped"   # 输出: 状态变化: running -> stopped

注意事项与常见错误

描述符必须定义为类属性

描述符必须定义中,不能定义实例中。如果写在 __init__ 中,它就是一个普通实例属性描述符协议不会生效。

__set_name__ 的作用

Python 3.6 引入了 __set_name__ 方法,当描述符定义中时自动调用参数属性名。这解决描述符不知道自己叫什么名字问题

避免在 __get__ 中返回 self

当 obj 为 None 时(通过类访问属性,如 MyClass.attr),通常应该返回描述符自身。否则 MyClass.attr 会报错。

数据存储位置

描述符通常不自己存储数据,而是把数据存在实例的 __dict__ 中。如果描述符自己存储数据,所有实例共享同一个

进阶用法

描述符与元类结合

使用元类自动注册描述符字段

class ModelMeta(type):
    """模型元类:自动收集字段"""
    def __new__(mcs, name, bases, namespace):
        fields = {}
        for key, value in namespace.items():
            if isinstance(value, Field):
                fields[key] = value
        namespace['_fields'] = fields
        return super().__new__(mcs, name, bases, namespace)

class Model(metaclass=ModelMeta):
    """模型基类"""
    pass

class User(Model):
    name = CharField(max_length=100)
    age = IntegerField()

# User._fields 自动包含 name 和 age

使用 __delete__ 方法

class CachedProperty:
    """可清除的缓存属性"""

    def __init__(self, func):
        self.func = func
        self.name = func.__name__

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        value = self.func(obj)
        obj.__dict__[self.name] = value
        return value

    def __delete__(self, obj):
        """删除缓存,下次访问时重新计算"""
        obj.__dict__.pop(self.name, None)

知识检测

学习进度

-- 已学文档
--% 知识覆盖率

学习推荐

专注模式