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

060、描述符协议:__get__、__set__、__delete__——property 的底层实现

060、描述符协议:getsetdelete——property 的底层实现

上周帮同事排查一个诡异的bug:某个类属性在并发环境下偶尔返回None,但明明构造函数里已经赋值了。翻代码看了半小时,发现他用@property装饰器包装了一个属性,但装饰器内部逻辑有分支没覆盖到。我盯着那段代码突然意识到——很多人用property用得飞起,但真问起描述符协议,十个有九个答不上来。

从一次调试说起

那个bug的简化版长这样:

classUser:def__init__(self,name):self._name=name@propertydefname(self):ifself._nameisNone:return"default"returnself._name

同事在某个线程里调了user.name = None,然后另一个线程读user.name,触发了property的getter,返回了"default"。他以为property会像普通属性一样直接返回None,但property的getter被触发了,逻辑走了默认分支。

这个问题的本质是:property不是普通属性,它是一个描述符。描述符协议决定了Python如何拦截属性的访问、赋值和删除操作。

描述符协议三剑客

描述符协议定义了三个魔法方法:

  • __get__(self, instance, owner):访问属性时触发
  • __set__(self, instance, value):赋值属性时触发
  • __delete__(self, instance):删除属性时触发

这里有个坑:只有实现了__set____delete__的描述符才是数据描述符,只实现__get__的是非数据描述符。数据描述符的优先级高于实例属性,非数据描述符的优先级低于实例属性。

别这样写——我曾经见过有人只实现了__get__,然后以为它能拦截赋值操作,结果赋值直接覆盖了描述符:

classReadOnlyDescriptor:def__get__(self,instance,owner):return42classMyClass:attr=ReadOnlyDescriptor()obj=MyClass()print(obj.attr)# 42obj.attr=100# 这里不会报错,直接覆盖了描述符print(obj.attr)# 100,描述符被实例属性覆盖了

这里踩过坑:想实现只读属性,必须同时实现__set__并抛出AttributeError

property的底层实现

property本质上是一个用C实现的数据描述符。我们可以用纯Python模拟一个简化版:

classMyProperty:def__init__(self,fget=None,fset=None,fdel=None,doc=None):self.fget=fget self.fset=fset self.fdel=fdelifdocisNoneandfgetisnotNone:doc=fget.__doc__ self.__doc__=docdef__get__(self,instance,owner):ifinstanceisNone:# 通过类访问时返回描述符本身returnselfifself.fgetisNone:raiseAttributeError("unreadable attribute")returnself.fget(instance)def__set__(self,instance,value):ifself.fsetisNone:raiseAttributeError("can't set attribute")self.fset(instance,value)def__delete__(self,instance):ifself.fdelisNone:raiseAttributeError("can't delete attribute")self.fdel(instance)defgetter(self,fget):returntype(self)(fget,self.fset,self.fdel,self.__doc__)defsetter(self,fset):returntype(self)(self.fget,fset,self.fdel,self.__doc__)defdeleter(self,fdel):returntype(self)(self.fget,self.fset,fdel,self.__doc__)

注意看__get__里的那个if instance is None判断——这里踩过坑:通过类访问属性时,instance是None,返回描述符对象本身。很多人不知道这个细节,导致调试时一脸懵逼。

描述符的查找顺序

Python的属性查找顺序是面试常考题,也是调试时最容易出问题的地方:

  1. 数据描述符(实现了__set____delete__
  2. 实例属性(__dict__
  3. 非数据描述符(只实现了__get__
  4. 类属性

这个顺序决定了为什么property能覆盖实例属性,而普通方法(非数据描述符)会被实例属性覆盖。

举个例子,你可能会遇到这种诡异情况:

classMyClass:defmethod(self):return"original"obj=MyClass()obj.method="overwritten"# 这里不会报错print(obj.method)# "overwritten",方法被实例属性覆盖了

因为函数是非数据描述符(只实现了__get__),实例属性的优先级更高。这就是为什么有时候你给实例绑了个同名属性,方法就调不到了。

实战:用描述符实现类型检查

我在实际项目中用描述符做类型检查,比用property写一堆重复代码优雅得多:

classTyped:def__init__(self,name,expected_type):self.name=name self.expected_type=expected_typedef__get__(self,instance,owner):ifinstanceisNone:returnselfreturninstance.__dict__.get(self.name)def__set__(self,instance,value):ifnotisinstance(value,self.expected_type):raiseTypeError(f"{self.name}must be{self.expected_type}")instance.__dict__[self.name]=valuedef__delete__(self,instance):delinstance.__dict__[self.name]classPerson:name=Typed("name",str)age=Typed("age",int)def__init__(self,name,age):self.name=name# 触发__set__self.age=age# 触发__set__p=Person("Alice",30)p.age="thirty"# TypeError: age must be <class 'int'>

这里有个设计细节:描述符里用instance.__dict__存储实际值,而不是在描述符对象内部存。这样每个实例都有自己的值,不会互相干扰。

描述符的陷阱与最佳实践

陷阱1:描述符是类属性,不是实例属性

classDescriptor:def__get__(self,instance,owner):return42classMyClass:attr=Descriptor()obj1=MyClass()obj2=MyClass()# obj1.attr和obj2.attr共享同一个描述符实例

别这样写——如果你在描述符内部存储状态,所有实例都会共享这个状态,除非你通过instance.__dict__来区分。

陷阱2:描述符的__set_name__

Python 3.6引入了__set_name__,可以在类创建时自动获取属性名:

classTyped:def__set_name__(self,owner,name):self.name=namedef__get__(self,instance,owner):ifinstanceisNone:returnselfreturninstance.__dict__.get(self.name)def__set__(self,instance,value):instance.__dict__[self.name]=valueclassPerson:name=Typed()# 自动获取属性名为"name"age=Typed()# 自动获取属性名为"age"

这里踩过坑:__set_name__只在类创建时调用一次,如果你动态修改类属性,它不会重新触发

个人经验建议

  1. 能用property就别手写描述符。property已经覆盖了90%的场景,手写描述符容易引入bug。我只有在需要复用逻辑(比如类型检查、日志记录)时才用描述符。

  2. 调试描述符问题时,先确认它是数据描述符还是非数据描述符。这个区别决定了属性查找顺序,很多诡异问题都出在这里。

  3. 描述符里不要缓存值在描述符对象上。除非你明确知道自己在做单例或类级别共享,否则永远用instance.__dict__存储实例数据。

  4. __set_name__是你的好朋友。在Python 3.6+的项目里,用它自动获取属性名,避免手动传参的重复劳动。

  5. 小心描述符的继承。子类继承父类的描述符时,描述符的__get__方法接收的owner参数是子类,不是父类。这个细节在实现某些框架功能时特别重要。

最后说一句:描述符是Python元编程的基石,理解了它,你才能真正理解property、classmethod、staticmethod这些装饰器的底层原理。下次遇到属性访问的诡异bug,先想想是不是描述符在搞鬼。

http://www.cnnetsun.cn/news/3023271.html

相关文章:

  • Seedance 2.5 要来了:普通人做自媒体,还需要自己拍素材吗?
  • 2026 电话机器人厂商测评及盘点:AI 外呼系统哪家更适合中小企业?
  • 硅基代办新浪潮:2026 年高阶 AI 生产力套件实测与选型指南
  • 如何快速解决Windows热键冲突:专业工具的完整指南
  • 收藏 | 从零入门:小白程序员必备的Loop Engineering大模型自动化实战指南
  • 目前正规的AI智能体APP哪家专业
  • Kali Linux实战:用SEToolkit克隆Pikachu靶场,模拟钓鱼攻击与防御
  • Fooocus:3分钟上手免费AI图像生成软件的终极指南
  • 流体控制中的模型降阶与预测控制:从高维仿真到实时优化
  • AlienFX-Tools:开源Alienware设备控制与性能优化解决方案
  • AMD Ryzen终极调试指南:如何用免费开源工具精准掌控处理器性能
  • 低成本入门无线电平台-【开元】Mini-SDR
  • 硕博写论文怕过不了盲审?Gradpaper 深度学术模型,适配学位论文 / 顶刊投稿标准
  • 开发者如何打造个人技术IP:从虚拟形象设计到自动化运营全攻略
  • 搭建个人游戏串流服务器:Sunshine完全指南让你在任何设备畅玩3A大作
  • 周纪三(第2部分,共2部分)
  • 郑州卫生间漏水怎么维修
  • 基于CSK6开发板的智能语音风扇控制方案
  • Beyond Compare 5授权机制深度解析:3种技术路径实现自定义密钥生成
  • 如何在5分钟内搭建自托管游戏串流服务器:Sunshine完整指南 [特殊字符]
  • 无限族双曲L-空间纽结构造:辫指数无界而隧道数恒为1
  • 第四:Python-UI自动化框架搭建(关键字驱动)
  • Swift图像背景移除终极指南:如何在iOS应用中快速实现智能抠图
  • 终极免费窗口强制调整工具:如何解决Windows顽固窗口尺寸问题
  • 有限维约化与射流逼近:从无限维PDE到可计算模型的桥梁
  • SAI拆分APK安装器:终极Android应用安装解决方案
  • AI幻觉治理实战:DeepRAG+RAT+神经符号混合架构
  • 计算机毕业设计之基于微信小程序的银行在线预约排号系统
  • qmc-decoder终极指南:快速解密QMC音频,释放音乐自由
  • 一站式Nintendo Switch游戏文件管理解决方案:NSC_BUILDER完全指南