Python描述符协议:属性访问的底层魔法

一、描述符协议:__get____set____delete__

描述符的本质

描述符是实现了特定协议的类,它控制对其他对象属性的访问。这是Python属性、方法、静态方法等特性的基础:

python 复制代码
class ValidatedAttribute:
    """数据验证描述符"""
    def __init__(self, validator):
        self.validator = validator
        self.attr_name = None  # 稍后由__set_name__设置
    
    def __set_name__(self, owner, name):
        """在描述符被赋值给类属性时调用(Python 3.6+)"""
        self.attr_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, None)
    
    def __set__(self, obj, value):
        """设置属性值时调用"""
        if not self.validator(value):
            raise ValueError(f"Invalid value for {self.attr_name}: {value}")
        setattr(obj, self.storage_name, value)
    
    def __delete__(self, obj):
        """删除属性时调用"""
        delattr(obj, self.storage_name)


def is_positive_number(value):
    """验证函数:必须是正数"""
    return isinstance(value, (int, float)) and value > 0


class Product:
    # 使用描述符
    price = ValidatedAttribute(is_positive_number)
    quantity = ValidatedAttribute(is_positive_number)
    
    def __init__(self, name, price, quantity):
        self.name = name
        self.price = price  # 触发__set__
        self.quantity = quantity  # 触发__set__

# 使用
try:
    p = Product("Apple", 10.5, 5)
    print(p.price)  # 10.5,触发__get__
    
    p.price = -5  # 触发__set__,抛出ValueError
except ValueError as e:
    print(f"验证错误: {e}")

p.quantity = 20  # 正常设置

二、描述符的类型

1. 数据描述符(实现__set____delete__

python 复制代码
class LoggedAttribute:
    """记录所有访问的数据描述符"""
    def __init__(self):
        self.data = {}
    
    def __get__(self, obj, objtype):
        if obj is None:
            return self
        print(f"读取 {obj}.{self.attr_name}")
        return self.data.get(id(obj))
    
    def __set__(self, obj, value):
        print(f"设置 {obj}.{self.attr_name} = {value}")
        self.data[id(obj)] = value
    
    def __set_name__(self, owner, name):
        self.attr_name = name

2. 非数据描述符(只实现__get__

python 复制代码
class CachedProperty:
    """属性缓存描述符(非数据描述符)"""
    def __init__(self, func):
        self.func = func
        self.name = func.__name__
    
    def __get__(self, obj, objtype):
        if obj is None:
            return self
        
        # 计算并缓存结果
        value = self.func(obj)
        obj.__dict__[self.name] = value  # 存储在实例字典中
        return value


class Circle:
    def __init__(self, radius):
        self.radius = radius
    
    @CachedProperty
    def area(self):
        print("计算面积...")
        return 3.14159 * self.radius ** 2
    
    @CachedProperty
    def circumference(self):
        print("计算周长...")
        return 2 * 3.14159 * self.radius

c = Circle(5)
print(c.area)  # 计算面积... 78.53975
print(c.area)  # 78.53975(从缓存读取,不再打印"计算面积...")

三、Python内置描述符的实现

1. property装饰器的实现原理

python 复制代码
class MyProperty:
    """简化版的property实现"""
    def __init__(self, fget=None, fset=None, fdel=None):
        self.fget = fget
        self.fset = fset
        self.fdel = fdel
    
    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        if self.fget is None:
            raise AttributeError("unreadable attribute")
        return self.fget(obj)
    
    def __set__(self, obj, value):
        if self.fset is None:
            raise AttributeError("can't set attribute")
        self.fset(obj, value)
    
    def __delete__(self, obj):
        if self.fdel is None:
            raise AttributeError("can't delete attribute")
        self.fdel(obj)
    
    def setter(self, fset):
        return type(self)(self.fget, fset, self.fdel)
    
    def deleter(self, fdel):
        return type(self)(self.fget, self.fset, fdel)


class Person:
    def __init__(self, name):
        self._name = name
    
    @MyProperty
    def name(self):
        return self._name
    
    @name.setter
    def name(self, value):
        if not value:
            raise ValueError("Name cannot be empty")
        self._name = value

p = Person("Alice")
print(p.name)  # Alice
p.name = "Bob"
print(p.name)  # Bob

2. 方法和静态方法的描述符实现

python 复制代码
import types

class MyMethod:
    """模拟方法描述符"""
    def __init__(self, func):
        self.func = func
    
    def __get__(self, obj, objtype):
        if obj is None:
            return self.func  # 通过类访问
        # 绑定方法:将实例作为第一个参数
        return types.MethodType(self.func, obj)


class MyStaticMethod:
    """模拟静态方法描述符"""
    def __init__(self, func):
        self.func = func
    
    def __get__(self, obj, objtype=None):
        # 始终返回原始函数
        return self.func


class MyClass:
    @MyMethod
    def instance_method(self):
        return f"实例方法,self={self}"
    
    @MyStaticMethod
    def static_method():
        return "静态方法"

obj = MyClass()
print(obj.instance_method())  # 实例方法,self=<__main__.MyClass object>
print(MyClass.static_method())  # 静态方法

四、描述符的应用场景

1. ORM(对象关系映射)框架

python 复制代码
class Field:
    """数据库字段描述符"""
    def __init__(self, field_type, nullable=True, default=None):
        self.field_type = field_type
        self.nullable = nullable
        self.default = default
        self.name = None
    
    def __set_name__(self, owner, name):
        self.name = name
    
    def __get__(self, obj, objtype):
        if obj is None:
            return self
        # 从实例的__dict__中获取值
        return obj.__dict__.get(self.name, self.default)
    
    def __set__(self, obj, value):
        if value is None and not self.nullable:
            raise ValueError(f"{self.name} cannot be null")
        if value is not None and not isinstance(value, self.field_type):
            raise TypeError(f"{self.name} must be {self.field_type}")
        obj.__dict__[self.name] = value


class ModelMeta(type):
    """元类,用于收集描述符"""
    def __new__(cls, name, bases, attrs):
        # 收集所有Field描述符
        fields = {}
        for key, value in attrs.items():
            if isinstance(value, Field):
                fields[key] = value
        
        attrs['_fields'] = fields
        return super().__new__(cls, name, bases, attrs)


class User(metaclass=ModelMeta):
    id = Field(int, nullable=False)
    name = Field(str, nullable=False)
    email = Field(str, default="")
    
    def __init__(self, **kwargs):
        for field_name, field in self._fields.items():
            value = kwargs.get(field_name, field.default)
            setattr(self, field_name, value)
    
    def to_dict(self):
        return {name: getattr(self, name) for name in self._fields}

# 使用
user = User(id=1, name="Alice")
print(user.to_dict())  # {'id': 1, 'name': 'Alice', 'email': ''}

2. 类型检查和自动转换

python 复制代码
class TypedAttribute:
    """类型检查和自动转换描述符"""
    def __init__(self, expected_type, converter=None):
        self.expected_type = expected_type
        self.converter = converter
        self.storage_name = None
    
    def __set_name__(self, owner, name):
        self.storage_name = f"_{name}"
    
    def __get__(self, obj, objtype):
        if obj is None:
            return self
        return getattr(obj, self.storage_name, None)
    
    def __set__(self, obj, value):
        if value is None:
            setattr(obj, self.storage_name, None)
            return
        
        # 尝试类型转换
        if self.converter and not isinstance(value, self.expected_type):
            try:
                value = self.converter(value)
            except (ValueError, TypeError):
                pass
        
        # 类型检查
        if not isinstance(value, self.expected_type):
            raise TypeError(
                f"Expected {self.expected_type.__name__}, "
                f"got {type(value).__name__}"
            )
        
        setattr(obj, self.storage_name, value)


class Config:
    port = TypedAttribute(int, converter=int)
    timeout = TypedAttribute(float, converter=float)
    debug = TypedAttribute(bool, converter=bool)
    
    def __init__(self, **kwargs):
        for key, value in kwargs.items():
            setattr(self, key, value)

cfg = Config(port="8080", timeout="30.5", debug="true")
print(cfg.port)    # 8080 (int)
print(cfg.timeout) # 30.5 (float)
print(cfg.debug)   # True (bool)

五、描述符的查找顺序

Python的属性查找链

python 复制代码
class Demo:
    """演示属性查找顺序"""
    class_descriptor = "类属性(描述符)"
    
    def __init__(self):
        self.instance_attr = "实例属性"
    
    def __getattr__(self, name):
        """找不到属性时调用"""
        return f"__getattr__: {name}"
    
    def __getattribute__(self, name):
        """总是首先调用"""
        print(f"__getattribute__: {name}")
        return super().__getattribute__(name)

class DataDescriptor:
    """数据描述符"""
    def __get__(self, obj, objtype):
        return "数据描述符"
    
    def __set__(self, obj, value):
        pass

class NonDataDescriptor:
    """非数据描述符"""
    def __get__(self, obj, objtype):
        return "非数据描述符"

class Test:
    data_desc = DataDescriptor()
    non_data_desc = NonDataDescriptor()

obj = Test()
obj.non_data_desc = "实例属性覆盖非数据描述符"

print(obj.data_desc)  # "数据描述符"(优先于实例属性)
print(obj.non_data_desc)  # "实例属性覆盖非数据描述符"

总结

描述符协议是Python最强大的特性之一,它实现了:

  1. 属性访问控制:验证、转换、记录属性访问
  2. 方法绑定机制:将函数转换为绑定方法
  3. 内置装饰器基础@property@classmethod@staticmethod
  4. 框架和库的核心:Django的Model字段、SQLAlchemy的Column等
相关推荐
亿牛云爬虫专家3 小时前
Worker越简单,系统越稳定:从单机到集群
爬虫·python·集群·爬虫代理·单机·代理ip·worker
smj2302_796826524 小时前
解决leetcode第3801题合并有序列表的最小成本
数据结构·python·算法·leetcode
AI数据皮皮侠4 小时前
中国乡村旅游重点村镇数据
大数据·人工智能·python·深度学习·机器学习
小北方城市网4 小时前
第 11 课:Python 全栈项目进阶与职业发展指南|从项目到职场的无缝衔接(课程终章・进阶篇)
大数据·开发语言·人工智能·python·数据库架构·geo
danyang_Q4 小时前
d2l安装(miniforge+cuda+pytorch)
人工智能·pytorch·python
源码梦想家5 小时前
多语言高性能异步任务队列与实时监控实践:Python、Java、Go、C++实战解析
开发语言·python
百***78755 小时前
Gemini 3.0 Pro与2.5深度对比:技术升级与开发实战指南
开发语言·python·gpt
reasonsummer5 小时前
【教学类-122-01】20260105“折纸-东南西北中”(4个方向文字,9个小图案)
python·通义万相
智航GIS5 小时前
9.6 JSON 基本操作
python·json
@zulnger6 小时前
python 学习笔记(文件读写)
笔记·python·学习