当前位置: 首页 > news >正文

Python 中的 __set__ 与 __set_name__ 深度解析

这两个方法都属于描述符协议(Descriptor Protocol),是 Python 元编程的核心机制之一。


一、描述符协议基础

描述符是实现了以下至少一个方法的类:

方法触发时机类型
__get__读取属性非数据描述符(若单独使用)
__set__设置属性数据描述符(含此方法)
__delete__删除属性数据描述符(含此方法)
__set_name__类定义时辅助方法(Python 3.6+)

二、__set__详解

签名

def__set__(self,obj,value):...
  • self:描述符实例本身
  • obj:拥有该描述符的类的实例
  • value:被赋予的值

核心机制

classDescriptor:def__get__(self,obj,objtype=None):print(f"__get__ called, obj={obj}")returnobj.__dict__.get('_value')def__set__(self,obj,value):print(f"__set__ called, value={value}")obj.__dict__['_value']=value# 注意:不能用 obj.attr = value,会无限递归!classMyClass:attr=Descriptor()m=MyClass()m.attr=42# 触发 __set__print(m.attr)# 触发 __get__

输出:

__set__ called, value=42 __get__ called, obj=<__main__.MyClass object> 42

⚠️ 数据描述符 vs 非数据描述符

属性查找优先级(MRO 之后):

数据描述符 > 实例 __dict__ > 非数据描述符
classNonData:"""只有 __get__,非数据描述符"""def__get__(self,obj,objtype=None):return"from descriptor"classData:"""有 __set__,数据描述符"""def__get__(self,obj,objtype=None):return"from descriptor"def__set__(self,obj,value):passclassA:x=NonData()y=Data()a=A()a.__dict__['x']="from instance"a.__dict__['y']="from instance"print(a.x)# "from instance" ← 实例 __dict__ 胜出print(a.y)# "from descriptor" ← 数据描述符胜出!

实战:类型验证描述符

classTypedField:def__init__(self,expected_type):self.expected_type=expected_type self.storage_name=None# 后面由 __set_name__ 填充def__set__(self,obj,value):ifnotisinstance(value,self.expected_type):raiseTypeError(f"'{self.storage_name}' 期望{self.expected_type.__name__},"f"得到{type(value).__name__}")obj.__dict__[self.storage_name]=valuedef__get__(self,obj,objtype=None):ifobjisNone:returnselfreturnobj.__dict__.get(self.storage_name)classPerson:name=TypedField(str)age=TypedField(int)p=Person()p.name="Alice"# ✅p.age=30# ✅p.age="thirty"# ❌ TypeError

三、__set_name__详解(Python 3.6+)

为什么需要它?

__set_name__出现之前,描述符不知道自己在宿主类中叫什么名字,只能手动传入:

# 旧方式(繁琐且容易出错)classPerson:name=TypedField(str,'name')# 必须手动重复写名字age=TypedField(int,'age')

签名

def__set_name__(self,owner,name):...
  • self:描述符实例
  • owner:拥有该描述符的(不是实例!)
  • name:该描述符在类中的属性名

触发时机

__set_name__类定义完成时由元类type.__new__自动调用,早于任何实例的创建。

classDescriptor:def__set_name__(self,owner,name):print(f"__set_name__ 被调用: owner={owner.__name__}, name='{name}'")self.name=nameclassMyClass:foo=Descriptor()# 类定义时立即触发bar=Descriptor()# 输出:# __set_name__ 被调用: owner=MyClass, name='foo'# __set_name__ 被调用: owner=MyClass, name='bar'

__set__联动:完整的描述符

classValidated:"""通用验证描述符,自动感知自身名称"""def__set_name__(self,owner,name):self.public_name=name# 外部访问名,如 'age'self.private_name='_'+name# 内部存储名,如 '_age'def__get__(self,obj,objtype=None):ifobjisNone:returnselfreturngetattr(obj,self.private_name,None)def__set__(self,obj,value):value=self.validate(value)setattr(obj,self.private_name,value)defvalidate(self,value):raiseNotImplementedErrorclassPositiveInt(Validated):defvalidate(self,value):ifnotisinstance(value,int)orvalue<=0:raiseValueError(f"必须是正整数,得到:{value!r}")returnvalueclassNonEmptyStr(Validated):defvalidate(self,value):ifnotisinstance(value,str)ornotvalue.strip():raiseValueError(f"不能为空字符串,得到:{value!r}")returnvalueclassProduct:name=NonEmptyStr()price=PositiveInt()stock=PositiveInt()p=Product()p.name="Python Book"p.price=99p.stock=10print(p.name,p.price,p.stock)# Python Book 99 10print(p.__dict__)# {'_name': 'Python Book', '_price': 99, '_stock': 10}

四、__set_name__的高级用法

1. 感知宿主类(owner)

classLoggedField:def__set_name__(self,owner,name):self.name=name# 在宿主类上注册所有被追踪的字段ifnothasattr(owner,'_tracked_fields'):owner._tracked_fields=[]owner._tracked_fields.append(name)def__set__(self,obj,value):print(f"[LOG]{type(obj).__name__}.{self.name}={value!r}")obj.__dict__[self.name]=valuedef__get__(self,obj,objtype=None):ifobjisNone:returnselfreturnobj.__dict__.get(self.name)classConfig:host=LoggedField()port=LoggedField()timeout=LoggedField()print(Config._tracked_fields)# ['host', 'port', 'timeout']c=Config()c.host="localhost"# [LOG] Config.host = 'localhost'c.port=8080# [LOG] Config.port = 8080

2. 动态添加描述符(手动调用__set_name__

如果在类定义之后动态添加描述符,需要手动调用__set_name__

classMyDesc:def__set_name__(self,owner,name):self.name=namedef__set__(self,obj,value):obj.__dict__[self.name]=value*2def__get__(self,obj,objtype=None):returnobj.__dict__.get(self.name)ifobjelseself# 动态挂载desc=MyDesc()MyClass.new_attr=desc desc.__set_name__(MyClass,'new_attr')# ⬅ 必须手动调用!m=MyClass()m.new_attr=10print(m.new_attr)# 20

五、与property的对比

property本身就是一个描述符,但描述符更灵活,可以复用:

# property:每个类属性都要写一次classCircle:@propertydefradius(self):returnself._radius@radius.setterdefradius(self,v):ifv<0:raiseValueError self._radius=v# 描述符:一次定义,到处复用classPositiveNumber:def__set_name__(self,owner,name):self.name=namedef__set__(self,obj,value):ifvalue<0:raiseValueError(f"{self.name}不能为负")obj.__dict__[self.name]=valuedef__get__(self,obj,objtype=None):returnobj.__dict__.get(self.name)ifobjelseselfclassCircle:radius=PositiveNumber()classRectangle:width=PositiveNumber()height=PositiveNumber()

六、总结

__set__(self, obj, value) └─ 在实例赋值时触发(obj.attr = value) └─ 使描述符成为"数据描述符",优先级高于实例 __dict__ └─ 存储值时用 obj.__dict__ 或 setattr(obj, private_name, value) __set_name__(self, owner, name) └─ 在类定义完成时由元类自动调用(一次性) └─ 提供描述符在类中的名称,解决自我引用问题 └─ 也可感知宿主类,用于注册、元数据收集等 └─ 动态挂载时需手动调用

两者配合使用是构建可复用、类型安全的属性系统的标准模式,也是dataclassesSQLAlchemyDjango ORM等框架底层的核心机制。

http://www.jsqmd.com/news/718828/

相关文章:

  • 收藏|2026 版 AI 大模型应用开发入门指南:行业转向、岗位解析、薪资曝光
  • 推荐一款神级 Claude Code 插件,暴涨 9.1 万 Star!
  • 如何掌握DLSS版本管理:DLSS Swapper完全指南与实战技巧
  • 免费开源工具终极指南:永久保存微信聊天记录的3种方法
  • 《产品路标规划与版本规划实践》深圳公开课(2026年6月12-13日)
  • LinkSwift网盘直链下载助手:八大网盘一键获取真实下载地址的技术解析与实践指南
  • 钢板防护罩品牌推荐:德州鑫姆迪克、盐山华蒴与国际品牌竞争力深度解析 - 品牌推荐大师
  • Firefly ROC-RK3588-RT开发板:高性能网络与边缘计算解析
  • 百度文库免费打印终极指南:三步获取纯净PDF文档的完整教程
  • 5分钟掌握暗黑2存档编辑:零基础打造你的完美游戏角色
  • 天虹购物卡回收流程详解,轻松变现零压力! - 团团收购物卡回收
  • Falcon监控与调试:10个实用工具和技巧
  • liquid-dsp快速开始教程:从安装到运行第一个调制解调示例
  • 如何一键捕获完整网页?这款免费Chrome扩展让你轻松搞定长网页截图
  • 3DTilesRendererJS社区贡献指南:如何参与NASA开源项目
  • 细聊人工智能获客,带飞智能品牌口碑好不好 - 工业设备
  • 千问3.5-2B图文理解参数详解:pad_token_id与eos_token_id在截断场景下的行为
  • 3个核心功能让WorkshopDL成为你的Steam创意工坊下载神器
  • 内存计算与XBTorch框架:深度学习硬件加速新范式
  • 调试UDS诊断通信必看:深入理解网络层六大超时参数(N_As, N_Bs, N_Cr...)与避坑指南
  • 告别文件管理混乱:Plane附件功能让项目协作效率提升300%
  • STM32F411CEU6上,用HAL库硬件IIC搞定MPU6050 DMP的完整流程(附代码避坑点)
  • 三步解锁百度文库:127行代码让你免费保存任何文档的终极指南
  • 国产vs进口信号隔离器深度对比:2026年在EMC性能、长期漂移与宽温工作下的表现 - 陈工日常
  • 如何用Deep3D将普通视频秒变3D大片?完整免费教程来了!
  • 终极指南:如何用NX代码所有权彻底解决团队协作中的责任难题
  • 抖音批量下载终极指南:5步掌握无水印内容下载技巧
  • 实用GTNH汉化指南:3分钟让Minecraft科技整合包变中文界面
  • 告别手动复制粘贴!用Python脚本批量提取ARXML文件里的ECU和信号信息(附完整代码)
  • #2026最新空调清洗消毒公司推荐!优质权威榜单发布,成都专业靠谱公司甄选 - 十大品牌榜