首页 / 知识库 / 0基础入门-阅读资料 / 0基础-python入门到精通

第 14 章 描述符

描述符是 Python 属性访问机制的底层实现,理解它就理解了 propertyclassmethodstaticmethod 的工作原理。

14.1 描述符协议

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

class Descriptor:
    def __get__(self, obj, objtype=None):
        """obj.attr 时调用"""
        ...
    
    def __set__(self, obj, value):
        """obj.attr = value 时调用"""
        ...
    
    def __delete__(self, obj):
        """del obj.attr 时调用"""
        ...

一个简单的描述符:

class Verbose:
    def __set_name__(self, owner, name):
        self.name = name
        self.storage_name = f"_verbose_{name}"
    
    def __get__(self, obj, objtype=None):
        if obj is None:
            return self   # 通过类访问时返回描述符本身
        value = getattr(obj, self.storage_name, None)
        print(f"获取 {self.name} = {value}")
        return value
    
    def __set__(self, obj, value):
        print(f"设置 {self.name} = {value}")
        setattr(obj, self.storage_name, value)

class MyClass:
    x = Verbose()
    y = Verbose()

obj = MyClass()
obj.x = 10       # 设置 x = 10
obj.y = 20       # 设置 y = 20
print(obj.x)     # 获取 x = 10 → 10

14.2 数据描述符 vs 非数据描述符

  • 数据描述符:定义了 __set__ 和/或 __delete__ 的描述符
  • 非数据描述符:只定义了 __get__ 的描述符

区别在于属性查找优先级

数据描述符 > 实例 __dict__ > 非数据描述符
class DataDescriptor:
    """数据描述符 — 有 __get__ 和 __set__"""
    def __get__(self, obj, objtype=None):
        return "来自数据描述符"
    def __set__(self, obj, value):
        print(f"数据描述符拦截了设置: {value}")

class NonDataDescriptor:
    """非数据描述符 — 只有 __get__"""
    def __get__(self, obj, objtype=None):
        return "来自非数据描述符"

class MyClass:
    data = DataDescriptor()
    nondata = NonDataDescriptor()

obj = MyClass()

# 数据描述符优先级高于实例 __dict__
obj.__dict__["data"] = "实例值"
print(obj.data)       # "来自数据描述符" — 描述符胜出

# 非数据描述符优先级低于实例 __dict__
obj.__dict__["nondata"] = "实例值"
print(obj.nondata)    # "实例值" — 实例字典胜出

完整的属性查找链

1. 数据描述符(类.__mro__ 中找到的、有 __set__ 或 __delete__ 的描述符)
2. 实例.__dict__
3. 非数据描述符(类.__mro__ 中找到的、只有 __get__ 的描述符)/ 类属性
4. __getattr__(如果定义了)
5. 抛出 AttributeError

14.3 描述符与 property、classmethod、staticmethod 的关系

这三个内置装饰器本质都是描述符:

用纯 Python 实现 property

class MyProperty:
    def __init__(self, fget=None, fset=None, fdel=None, doc=None):
        self.fget = fget
        self.fset = fset
        self.fdel = fdel
        self.__doc__ = doc or (fget.__doc__ if fget else None)
    
    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        if self.fget is None:
            raise AttributeError("不可读")
        return self.fget(obj)
    
    def __set__(self, obj, value):
        if self.fset is None:
            raise AttributeError("不可写")
        self.fset(obj, value)
    
    def __delete__(self, obj):
        if self.fdel is None:
            raise AttributeError("不可删")
        self.fdel(obj)
    
    def getter(self, fget):
        return type(self)(fget, self.fset, self.fdel)
    
    def setter(self, fset):
        return type(self)(self.fget, fset, self.fdel)
    
    def deleter(self, fdel):
        return type(self)(self.fget, self.fset, fdel)

用纯 Python 实现 staticmethod

class MyStaticMethod:
    def __init__(self, func):
        self.func = func
    
    def __get__(self, obj, objtype=None):
        return self.func   # 直接返回原函数,不绑定任何东西

用纯 Python 实现 classmethod

class MyClassMethod:
    def __init__(self, func):
        self.func = func
    
    def __get__(self, obj, objtype=None):
        if objtype is None:
            objtype = type(obj)
        # 返回一个绑定了 cls 的函数
        def wrapper(*args, **kwargs):
            return self.func(objtype, *args, **kwargs)
        return wrapper

函数本身也是描述符

Python 的普通函数实现了 __get__,这就是方法绑定的秘密:

class MyClass:
    def method(self):
        pass

obj = MyClass()

# 通过类访问 — 得到函数
print(MyClass.__dict__["method"])    # <function MyClass.method at 0x...>

# 通过实例访问 — 函数的 __get__ 返回绑定方法
print(obj.method)  # <bound method MyClass.method of <MyClass object>>

# 手动调用函数的 __get__
func = MyClass.__dict__["method"]
bound_method = func.__get__(obj, MyClass)
print(bound_method)  # <bound method MyClass.method of <MyClass object>>

14.4 用描述符实现 ORM 字段验证

class Field:
    """ORM 字段基类"""
    def __set_name__(self, owner, name):
        self.name = name
        self.storage_name = f"_{name}"
    
    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        return getattr(obj, self.storage_name, self.default)
    
    def __set__(self, obj, value):
        value = self.validate(value)
        setattr(obj, self.storage_name, value)
    
    def validate(self, value):
        return value  # 子类重写

class StringField(Field):
    def __init__(self, min_length=0, max_length=None, default=""):
        self.min_length = min_length
        self.max_length = max_length
        self.default = default
    
    def validate(self, value):
        if not isinstance(value, str):
            raise TypeError(f"{self.name} 必须是字符串")
        if len(value) < self.min_length:
            raise ValueError(f"{self.name} 长度不能少于 {self.min_length}")
        if self.max_length and len(value) > self.max_length:
            raise ValueError(f"{self.name} 长度不能超过 {self.max_length}")
        return value

class IntField(Field):
    def __init__(self, min_value=None, max_value=None, default=0):
        self.min_value = min_value
        self.max_value = max_value
        self.default = default
    
    def validate(self, value):
        if not isinstance(value, int):
            raise TypeError(f"{self.name} 必须是整数")
        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}")
        return value

class EmailField(StringField):
    def validate(self, value):
        value = super().validate(value)
        if "@" not in value:
            raise ValueError(f"{self.name} 不是有效的邮箱地址")
        return value

# 使用
class User:
    name = StringField(min_length=1, max_length=50)
    age = IntField(min_value=0, max_value=150)
    email = EmailField(min_length=5)
    
    def __init__(self, name, age, email):
        self.name = name
        self.age = age
        self.email = email
    
    def __repr__(self):
        return f"User(name={self.name!r}, age={self.age}, email={self.email!r})"

user = User("Alice", 30, "alice@example.com")
print(user)  # User(name='Alice', age=30, email='alice@example.com')

# 验证生效
try:
    user.name = ""  # ValueError: name 长度不能少于 1
except ValueError as e:
    print(e)

try:
    user.age = -1   # ValueError: age 不能小于 0
except ValueError as e:
    print(e)

try:
    user.email = "invalid"  # ValueError: email 不是有效的邮箱地址
except ValueError as e:
    print(e)

本章小结:描述符是 Python 属性访问机制的核心,是 propertyclassmethodstaticmethod 和方法绑定的底层实现。理解数据描述符和非数据描述符的优先级差异,就理解了 Python 的属性查找链。描述符在实现 ORM、验证框架等场景中非常强大。