资讯动态

python中的属性管理机制详解

发布时间:2026/10/10 21:21:14 来源:尧图企业网站定制
前言「属性管理」这个词在 Python 里没有官方定义它对应的是一整条属性访问协议attribute access protocol读属性、写属性、删属性、找不到属性时怎么办以及这些动作如何被自定义对象接管。把这条协议摸清楚就能理解property、ORM 的字段回填、__slots__这些常见机制底下的同一套原理。常见的误解有三个把__getattr__和__getattribute__当成同一个东西其实触发时机完全不同以为重写__setattr__就能拦住所有写操作其实它会连自己的初始化都一起拦以为property是某种特殊语法糖其实它就是一个数据描述符。本文只讲属性访问协议这一条线四个钩子各自的职责、__getattr__与__getattribute__的差别、描述符协议的__get__/__set__以及property为什么是数据描述符。代码按 Python 3.8 给出。一、四个钩子的分工数据模型里为「自定义属性访问」准备了四个特殊方法钩子何时被调用典型用途__getattribute__(self, name)无条件每次读属性都调用完全接管读属性__getattr__(self, name)常规查找失败、抛AttributeError时兜底、动态属性__setattr__(self, name, value)每次赋值属性时校验、记录、代理__delattr__(self, name)每次删除属性时校验、清理关联资源注意object本身没有提供__getattr__它只提供__getattribute__。所以object.__getattr__不是可用的兜底实现要兜底就得自己定义。二、__getattr__ 与 __getattribute__ 的触发时机这是本节最核心的区分。__getattribute__是「无条件」的只要你写obj.x它就先跑。__getattr__只在「常规机制失败」时才被调用——具体说是__getattribute__抛出了AttributeError之后。# 适用于 Python 3.8class Config:debug Truedef __getattr__(self, name):# 只有常规查找失败时才会走到这里return f未定义的配置项 {name}c Config()print(c.debug) # 推演输出True —— 常规命中__getattr__ 不触发print(c.timeout) # 推演输出未定义的配置项 timeoutdebug是一个真实存在的类属性常规查找成功__getattr__根本不参与。timeout谁都没有常规查找抛AttributeError才轮到__getattr__返回一个「假装存在」的值。如果要连已存在的属性也拦下来就必须用__getattribute__# 适用于 Python 3.8class Traced:def __getattribute__(self, name):print(f读取属性{name})# 必须调用基类实现否则会无限递归return object.__getattribute__(self, name)t Traced()t.x 1print(t.x)# 推演输出# 读取属性x# 1这里有两处必须小心一是每一层__getattribute__都必须转调object.__getattribute__(self, name)二是绝不能在__getattribute__里用self.something那会再次触发自己栈溢出。三、__setattr__ 与 __delattr____setattr__取代了「把值写进实例字典」这个默认动作。官方文档提到一个关键点如果要在__setattr__里完成正常的赋值必须调用基类实现object.__setattr__(self, name, value)直接写self.name value会递归调用自己。# 适用于 Python 3.8class Positive:def __setattr__(self, name, value):if name age and value 0:raise ValueError(age 不能为负)object.__setattr__(self, name, value) # 交回默认动作p Positive()p.age 30print(p.age) # 推演输出30try:p.age -1except ValueError as exc:print(拦截, exc) # 推演输出拦截 age 不能为负__delattr__是__setattr__的删除版只在del obj.name有意义时才实现同样应转调基类。四、描述符协议与 property 的底层原理描述符descriptor是「定义了__get__、__set__、__delete__中任意一个方法的对象」。当它作为类变量存在时通过实例访问它就会触发这些方法descr.__get__(self, obj, typeNone)descr.__set__(self, obj, value)descr.__delete__(self, obj)按定义分成两类数据描述符data descriptor定义了__set__或__delete__。它优先于实例字典。非数据描述符non-data descriptor只定义了__get__。它会被实例字典覆盖。实例查找的完整优先级是数据描述符 → 实例变量 → 非数据描述符 → 类变量 →__getattr__。想做一个只读的数据描述符就同时定义__get__和「抛AttributeError的__set__」。# 适用于 Python 3.8class PositiveNumber:def __set_name__(self, owner, name):self.name name # 3.6 自动被告知自己的属性名def __get__(self, obj, objtypeNone):if obj is None:return self # 通过类访问时返回描述符本身return obj.__dict__.get(self.name)def __set__(self, obj, value):if value 0:raise ValueError(f{self.name} 必须非负)obj.__dict__[self.name] valueclass Order:price PositiveNumber() # 类变量位置的描述符qty PositiveNumber()def __init__(self, price, qty):self.price price # 走 __set__self.qty qtyo Order(3, 2)print(o.price, o.qty) # 推演输出3 2try:o.qty -1except ValueError as exc:print(拦截, exc) # 推演输出拦截 qty 必须非负__set_name__在 3.6 起由type.__new__在类创建时自动调用PEP 487省去了手动把属性名从__set__的参数里剖出来的麻烦。property 就是数据描述符property不是什么特殊语法。官方描述符指南给出的等价纯 Python 实现里Property类同时定义了__get__、__set__、__delete__和__set_name__——也就是说property正是一个标准的数据描述符。这就解释了两个常见现象为什么property定义的属性不会被实例字典覆盖因为数据描述符优先级最高为什么只写 getter 而不写 setter 时赋值会抛AttributeError因为对应的fset是None__set__直接抛错。机制底层是什么谁优先obj.__dict__里的普通值普通实例属性仅次于数据描述符类里的普通变量类属性低于实例属性类里的函数非数据描述符会被实例字典覆盖property数据描述符最高压过实例字典__slots__生成的成员数据描述符最高压过实例字典顺带说清 Python 2 的旧式类Python 2.7 已于2020 年 1 月 1 日停止维护PEP 373而且上面这套协议在 Python 2 里只对新式类new-style class显式继承object成立。Python 2 的旧式类old-style / classic class在对象模型上是另一套东西type(x)得到的是type instance而不是类本身__getattribute__只对新式类生效官方文档明确写着「只有新式类支持描述符」。因此把property或自制描述符挂到旧式类上取值时拿到的是描述符对象本身而不是计算出来的值。Python 2 里还有一处写法上的硬差别super()必须写全参数super(C, self)零参数写法是 Python 3 才有的。迁移到 Python 3 时class C(object):可以放心写成class C:因为 Python 3 里所有类都是新式类旧式类已经被彻底移除。常见坑点1. 以为__getattr__能拦住所有属性读取。❌ 定义__getattr__后期待它也能拦下已存在的属性。 ✅ 已存在的属性走常规查找根本不触发__getattr__要全拦请用__getattribute__。2. 在__getattribute__里用self.名字直接取属性。❌def __getattribute__(self, name): return self.__dict__[name]一跑就递归爆栈。 ✅ 转调object.__getattribute__(self, name)需要字典时用object.__getattribute__(self, __dict__)。3. 在__setattr__里写self.x v。❌ 想保存值结果又调进了自己的__setattr__无限递归。 ✅ 用object.__setattr__(self, name, value)交回默认实现。4. 把非数据描述符当「默认值」用结果被实例属性悄悄压过。❌ 只实现__get__的校验器第一次self.x 5之后校验就永久失效了。 ✅ 需要参与赋值的描述符必须实现__set__让它成为数据描述符。5.__getattr__忘了抛AttributeError。❌ 任何名字都返回一个值于是hasattr恒为真、copy与序列化行为错乱。 ✅ 只对真正认识的名字返回其余一律raise AttributeError(name)。6.__delattr__里不转调基类。❌ 自己实现删除逻辑却不去动实例字典del obj.x之后obj.x竟然还在。 ✅ 需要默认行为时调用object.__delattr__(self, name)。7. 以为重写__getattribute__能拦住内置语法的属性访问。❌ 期待重写后len(obj)、obj 1也会走自己的__getattribute__。 ✅ 特殊方法的隐式查找走的是类型上的槽位绕过__getattribute__这是官方的既定行为。总结问题结论读属性先走谁__getattribute__无条件找不到时才走谁__getattr__写属性走谁__setattr__想保留默认行为就转调object删属性走谁__delattr__描述符分几类数据描述符有__set__/__delete__、非数据描述符只有__get__优先级数据描述符 实例字典 非数据描述符 类变量 __getattr__property是什么一个数据描述符一句话收尾属性管理的全部秘密就藏在「谁先谁后」这条优先级链里property、__slots__、ORM 字段都是同一条链上的不同包装。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑