前置知识: Python

描述符

3 minAdvanced2026/6/14

Python描述符协议详解:__get__、__set__、__delete__。

概述

描述符(Descriptor)是 Python 中实现属性访问控制的核心协议。任何实现了 __get____set____delete__ 方法的都是描述符。描述符是 property、classmethod、staticmethod 等内置功能的底层机制,也是 ORM 框架中字段定义的基础。

基础概念

描述符协议

描述符协议包含以下方法:

  • __get__(self, obj, objtype=None):访问属性时调用
  • __set__(self, obj, value):设置属性时调用
  • __delete__(self, obj):删除属性时调用

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

  • 数据描述符(Data Descriptor):实现了 __set____delete__ 的描述符
  • 非数据描述符(Non-Data Descriptor):只实现了 __get__ 的描述符

两者的关键区别在于优先级:数据描述符的优先级高于实例属性,非数据描述符的优先级低于实例属性。

# 数据描述符:优先级高于实例属性
class Validated:
    def __get__(self, obj, objtype=None):
        return getattr(obj, self.name, None)

    def __set__(self, obj, value):
        setattr(obj, self.name, value)

# 非数据描述符:优先级低于实例属性
class Lazy:
    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        return self.compute(obj)

    def compute(self, obj):
        return "计算结果"

属性查找顺序

Python 的属性查找遵循以下优先级:

  1. 数据描述符(上的 __get__ + __set__
  2. 实例属性(obj.__dict__
  3. 非数据描述符(上的 __get__
  4. 属性

快速上手

第一个描述符

class TypedField:
    """类型检查描述符"""
    def __init__(self, expected_type, default=None):
        self.expected_type = expected_type
        self.default = default

    def __set_name__(self, owner, name):
        """Python 3.6+:自动获取属性名"""
        self.name = f"_{name}"

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

    def __set__(self, obj, value):
        if not isinstance(value, self.expected_type):
            raise TypeError(
                f"期望 {self.expected_type.__name__},得到 {type(value).__name__}"
            )
        setattr(obj, self.name, value)

class Person:
    name = TypedField(str, "")
    age = TypedField(int, 0)

p = Person()
p.name = "Alice"  # OK
# p.name = 42     # TypeError: 期望 str,得到 int
p.age = 30        # OK

property 是描述符

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("半径不能为负")
        self._radius = value

# 等价的描述符实现
class Radius:
    def __get__(self, obj, objtype=None):
        return obj._radius

    def __set__(self, obj, value):
        if value < 0:
            raise ValueError("半径不能为负")
        obj._radius = value

详细用法

验证描述符

class Range:
    """范围验证描述符"""
    def __init__(self, min_val=None, max_val=None):
        self.min_val = min_val
        self.max_val = max_val

    def __set_name__(self, owner, name):
        self.name = f"_{name}"

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

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

class Student:
    score = Range(0, 100)
    age = Range(0, 150)

s = Student()
s.score = 95   # OK
# s.score = -1  # ValueError: 值不能小于 0
# s.score = 200 # ValueError: 值不能大于 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)
        setattr(obj, self.name, value)
        return value

class DataLoader:
    def __init__(self, path):
        self.path = path

    @LazyProperty
    def data(self):
        """首次访问时加载数据,之后使用缓存"""
        print("正在加载数据...")
        with open(self.path) as f:
            return f.read()

loader = DataLoader("data.txt")
print(loader.data)  # 输出: 正在加载数据... + 数据内容
print(loader.data)  # 直接返回缓存,不再加载

委托描述符

class Alias:
    """属性别名描述符"""
    def __init__(self, target):
        self.target = target

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

    def __set__(self, obj, value):
        setattr(obj, self.target, value)

class User:
    def __init__(self, first_name, last_name):
        self.first_name = first_name
        self.last_name = last_name

    name = Alias("first_name")  # name 是 first_name 的别名

u = User("Alice", "Smith")
print(u.name)    # Alice
u.name = "Bob"
print(u.first_name)  # Bob

方法描述符

函数本身就是非数据描述符,这就是方法绑定的工作原理:

class MyClass:
    def method(self):
        return "实例方法"

# 函数的 __get__ 实现了方法绑定
obj = MyClass()
print(type(MyClass.method))  # function(未绑定)
print(type(obj.method))      # method(绑定到 obj)

常见场景

场景一:ORM 字段

class Column:
    """数据库列描述符"""
    def __init__(self, col_type, primary_key=False, nullable=True):
        self.col_type = col_type
        self.primary_key = primary_key
        self.nullable = nullable

    def __set_name__(self, owner, name):
        self.name = name
        self.storage = f"_{name}_value"

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

    def __set__(self, obj, value):
        if value is None and not self.nullable:
            raise ValueError(f"{self.name} 不能为空")
        if value is not None and not isinstance(value, self.col_type):
            raise TypeError(f"{self.name} 类型错误")
        setattr(obj, self.storage, value)

class User:
    id = Column(int, primary_key=True, nullable=False)
    name = Column(str, nullable=False)
    email = Column(str, nullable=True)

场景二:缓存属性

class Cached:
    """带过期时间的缓存描述符"""
    def __init__(self, ttl=60):
        self.ttl = ttl

    def __set_name__(self, owner, name):
        self.name = name
        self.cache_key = f"_{name}_cache"
        self.time_key = f"_{name}_time"

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

        import time
        last_time = getattr(obj, self.time_key, 0)
        if time.time() - last_time > self.ttl:
            return None  # 缓存过期

        return getattr(obj, self.cache_key, None)

    def __set__(self, obj, value):
        import time
        setattr(obj, self.cache_key, value)
        setattr(obj, self.time_key, time.time())

场景三:只读属性

class ReadOnly:
    """只读描述符:初始化后不可修改"""
    def __set_name__(self, owner, name):
        self.name = f"_{name}"

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

    def __set__(self, obj, value):
        if hasattr(obj, self.name):
            raise AttributeError(f"属性 {self.name[1:]} 是只读的")
        setattr(obj, self.name, value)

class Config:
    host = ReadOnly()
    port = ReadOnly()

c = Config()
c.host = "localhost"  # 首次设置 OK
# c.host = "other"    # AttributeError: 属性 host 是只读的

注意事项

  • 描述符必须定义为属性,定义在实例上不会生效
  • __set_name__ 在 Python 3.6+ 可用,之前版本需要手动传递属性名
  • 数据描述符的 __get__ 在访问时总是被调用,即使实例属性存在
  • 非数据描述符可以被实例属性覆盖
  • 描述符中存储值时不要使用与描述符同名的属性,否则会导致无限递归
  • 使用 obj.__dict__ 或带前缀的属性名存储实际值

进阶用法

描述符与元配合

class ModelMeta(type):
    """收集所有描述符字段"""
    def __new__(mcs, name, bases, namespace):
        fields = {}
        for key, value in namespace.items():
            if isinstance(value, Column):
                fields[key] = value
        namespace['_fields'] = fields
        return super().__new__(mcs, name, bases, namespace)

class Model(metaclass=ModelMeta):
    def validate(self):
        """验证所有字段"""
        for name, field in self._fields.items():
            value = getattr(self, name)
            if value is None and not field.nullable:
                raise ValueError(f"{name} 不能为空")

描述符链

class Validated:
    """可组合的验证描述符"""
    def __init__(self, *validators):
        self.validators = validators

    def __set_name__(self, owner, name):
        self.name = f"_{name}"

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

    def __set__(self, obj, value):
        for validator in self.validators:
            validator(value)  # 执行验证
        setattr(obj, self.name, value)

def positive(value):
    if value <= 0:
        raise ValueError("值必须为正数")

def even(value):
    if value % 2 != 0:
        raise ValueError("值必须是偶数")

class Settings:
    count = Validated(positive, even)

s = Settings()
s.count = 4   # OK
# s.count = -2  # ValueError: 值必须为正数
# s.count = 3   # ValueError: 值必须是偶数