news 2026/6/26 19:57:04

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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,先想想是不是描述符在搞鬼。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/26 19:56:20

Seedance 2.5 要来了:普通人做自媒体,还需要自己拍素材吗?

很多人做自媒体&#xff0c;最先卡住的不是账号&#xff0c;也不是平台&#xff0c;而是内容做不出来。 脑子里有选题&#xff0c;但没有画面。 想做一条短视频&#xff0c;但没有素材。 想帮小店做宣传&#xff0c;但拍摄成本太高。 想追一个热点&#xff0c;但等素材找齐&…

作者头像 李华
网站建设 2026/6/26 19:55:18

2026 电话机器人厂商测评及盘点:AI 外呼系统哪家更适合中小企业?

2026 年&#xff0c;AI 电话机器人已经从传统“自动拨号 固定话术”升级到具备大模型理解、多轮对话、意向识别和销售线索交接能力的 AI 电话员工。本文从技术能力、适用企业、部署方式、成本门槛和试用友好度等维度&#xff0c;盘点知微联智 AI 电话员工、科大讯飞 AI 外呼、…

作者头像 李华
网站建设 2026/6/26 19:55:11

硅基代办新浪潮:2026 年高阶 AI 生产力套件实测与选型指南

步入 2026 年&#xff0c;生成式技术的技术红利正在全面沉降。评估一款 AI 工具是否好用&#xff0c;行业标准已不再看它能组织多么华丽的辞藻&#xff0c;而看它能否精准嵌入严苛的现实工作流&#xff0c;并解决企业与独立创作者的产能红线。 当单一的聊天框无法满足高频的复…

作者头像 李华
网站建设 2026/6/26 19:55:03

如何快速解决Windows热键冲突:专业工具的完整指南

如何快速解决Windows热键冲突&#xff1a;专业工具的完整指南 【免费下载链接】hotkey-detective A small program for investigating stolen key combinations under Windows 7 and later. 项目地址: https://gitcode.com/gh_mirrors/ho/hotkey-detective 你是否曾经遇…

作者头像 李华
网站建设 2026/6/26 19:49:28

目前正规的AI智能体APP哪家专业

说实话&#xff0c;这个问题挺难回答的。我在这个圈子里泡了五年&#xff0c;见过太多产品——有的界面花哨得像科幻电影&#xff0c;结果一问三不知&#xff1b;有的号称“全能助手”&#xff0c;实际连最简单的工作流都跑不通。用户很容易踩坑&#xff0c;下载一堆发现都是玩…

作者头像 李华