时间:2025-12-31 11:24
人气:
作者:admin
__get__、__set__、__delete__ 任意一个或多个方法的Python类(这三个方法被称为“描述器协议”)。
核心作用:控制属性的访问、赋值、删除行为,实现属性的精细化管控(如类型校验、值范围限制、懒加载等)。
分类:
__get__ 和 __set____get__class Descriptor: def __get__(self, instance, owner): """ 访问属性时触发 :param instance: 拥有该描述器属性的实例对象(如obj),若通过类访问则为None :param owner: 拥有该描述器属性的类(如Cls) :return: 要返回的属性值 """ pass def __set__(self, instance, value): """ 给属性赋值时触发 :param instance: 实例对象(必传,不能通过类赋值触发) :param value: 要赋值的值 """ pass def __delete__(self, instance): """ 删除属性时触发(del obj.attr) :param instance: 实例对象 """ pass
class IntField: # 实现__get__和__set__,成为数据描述器 def __get__(self, instance, owner): # 这里用instance.__dict__存储实际值,避免触发__get__递归 return instance.__dict__.get(self, None) def __set__(self, instance, value): # 类型校验(核心功能) if not isinstance(value, int): raise TypeError(f"属性值必须是整数,当前传入:{type(value)}") # 把值存入实例的__dict__,key用self(描述器实例本身) instance.__dict__[self] = value # 测试:使用描述器 class User: # 给User类定义两个IntField属性 age = IntField() score = IntField() # 正常赋值 u1 = User() u1.age = 25 # 合法 u1.score = 90 # 合法 print(u1.age, u1.score) # 输出:25 90 # 异常赋值(触发类型校验) try: u1.age = "25" # 传入字符串 except TypeError as e: print(e) # 输出:属性值必须是整数,当前传入:<class 'str'>关键说明:用
instance.__dict__[self] 存储值,而非直接 instance.attr = value,避免赋值时再次触发 __set__ 导致递归调用。
# 1. 定义数据描述器 class DataDesc: def __get__(self, instance, owner): return "DataDesc的__get__被触发" def __set__(self, instance, value): instance.__dict__[self] = value # 2. 定义非数据描述器 class NonDataDesc: def __get__(self, instance, owner): return "NonDataDesc的__get__被触发" # 3. 测试类 class Test: # 类属性:数据描述器、非数据描述器 data_desc = DataDesc() non_data_desc = NonDataDesc() # 普通类属性 cls_attr = "普通类属性" t = Test() # 验证1:数据描述器 > 实例属性 t.data_desc = "我是实例属性" # 给实例赋值(本应存入__dict__) print(t.data_desc) # 输出:DataDesc的__get__被触发(数据描述器优先,忽略实例属性) print(t.__dict__.get("data_desc")) # 输出:None(赋值被__set__拦截,未存入实例__dict__) # 验证2:实例属性 > 非数据描述器 t.non_data_desc = "我是实例属性" # 给实例赋值 print(t.non_data_desc) # 输出:我是实例属性(实例属性优先,非数据描述器失效) del t.non_data_desc # 删除实例属性 print(t.non_data_desc) # 输出:NonDataDesc的__get__被触发(实例属性删除后,非数据描述器生效) # 验证3:非数据描述器 > 普通类属性 print(t.cls_attr) # 输出:普通类属性(无实例属性时,访问类属性) # 动态添加非数据描述器到类 Test.cls_attr = NonDataDesc() print(t.cls_attr) # 输出:NonDataDesc的__get__被触发(非数据描述器优先)
import time class LazyLoad: def __init__(self, load_func): # 接收一个加载函数(负责实际的耗时操作) self.load_func = load_func def __get__(self, instance, owner): # 第一次访问时,执行加载函数获取值 value = self.load_func() # 把加载后的值存入实例__dict__(用属性名作为key) # 这里通过instance.__dict__[self.load_func.__name__]绑定,避免重复加载 instance.__dict__[self.load_func.__name__] = value return value # 模拟耗时操作(如数据库查询) def load_user_info(): print("开始加载用户信息(耗时操作)...") time.sleep(2) # 模拟耗时 return {"name": "张三", "id": 1001} # 模拟耗时操作(如读取大文件) def load_file_content(): print("开始读取大文件(耗时操作)...") time.sleep(1) return "大文件内容..." # 测试类 class UserInfo: # 用LazyLoad描述器绑定耗时属性 user_info = LazyLoad(load_user_info) file_content = LazyLoad(load_file_content) # 实例化(此时不触发耗时操作) ui = UserInfo() print("实例创建完成,未触发加载") # 第一次访问user_info(触发加载) print(ui.user_info) # 输出:开始加载用户信息(耗时操作)... 然后输出字典 # 第二次访问user_info(直接从实例__dict__获取,不触发加载) print(ui.user_info) # 直接输出字典,无耗时 # 访问file_content(触发加载) print(ui.file_content) # 输出:开始读取大文件(耗时操作)... 然后输出内容优势:减少实例初始化时间,尤其适合有多个耗时属性的类(如ORM模型、大数据处理类)。
@property 装饰器,底层就是用描述器实现的。下面用描述器复刻一个简易版property:
class MyProperty: def __init__(self, fget=None, fset=None, fdel=None): # 接收getter、setter、deleter函数 self.fget = fget self.fset = fset self.fdel = fset def __get__(self, instance, owner): if self.fget: return self.fget(instance) def __set__(self, instance, value): if self.fset: self.fset(instance, value) else: raise AttributeError("该属性不可赋值") # 实现装饰器的setter方法(模仿@property.setter) def setter(self, func): self.fset = func return self # 用MyProperty替代@property class Person: def __init__(self): self._name = None # 私有变量 # 用MyProperty定义name属性 @MyProperty def name(self): return self._name # 用MyProperty.setter定义赋值逻辑 @name.setter def name(self, value): if not isinstance(value, str): raise TypeError("名字必须是字符串") self._name = value # 测试 p = Person() p.name = "李四" # 触发MyProperty.__set__ print(p.name) # 触发MyProperty.__get__,输出:李四 try: p.name = 123 # 非字符串,触发异常 except TypeError as e: print(e) # 输出:名字必须是字符串结论:
@property 本质是对描述器的封装,让我们无需手动实现 __get__/__set__ 就能实现属性管控。
models.IntegerField、models.CharField,底层用描述器实现字段类型校验、数据转换(数据库类型<->Python类型)。__get__/__set__ 中直接访问 instance.attr 会再次触发描述器,导致递归栈溢出,需用 instance.__dict__ 直接操作。class User: age = IntField()),若定义为实例属性则无法生效。instance.__dict__(不推荐)。
石家庄的.net程序员