Python 的 obj.x 到底按什么顺序找?拆开数据描述符、实例字典和 __getattr__ 那条优先级链

简介: 一句 obj.x 最快 22.1ns、最慢 226.5ns,差十倍。本文拆开 CPython 属性查找的五层优先级链:数据描述符 > 实例字典 > 非数据描述符 > 类属性 > __getattr__,用有没有 __set__ 判定分水岭,解释 property 为何盖不住、实例属性为何能覆盖方法,附四档 timeit 实测与 getattr_static 调试法

先看一组我在自己机器上跑出来的数(CPython 3.13.14,timeit 200 万次取平均):

普通实例属性             22.1 ns/次
非数据描述符(实例字典赢) 57.1 ns/次
数据描述符               137.6 ns/次
__getattr__ 兜底         191.5 ns/次
覆写 __getattribute__    226.5 ns/次

同样一句 obj.x,最慢和最快差了十倍。差别不在字典查得快不快,而在这一句背后有一条五层的优先级链,Python 得一层层往下试。

更反直觉的是:我把值塞进了 obj.__dict__,取出来却可能不是它。

一、一个「塞进去也白塞」的实验

class DataDesc:
    def __get__(self, obj, owner): return 'DATA_DESC'
    def __set__(self, obj, v): pass          # 关键:有 __set__

class NonDataDesc:
    def __get__(self, obj, owner): return 'NONDATA_DESC'
    # 没有 __set__

class K:
    d = DataDesc()
    n = NonDataDesc()
    cls_attr = 'CLASS_ATTR'
    def __getattr__(self, name):
        return 'GETATTR:' + name

k = K()
k.__dict__['d']        = 'INSTANCE_DICT'
k.__dict__['n']        = 'INSTANCE_DICT'
k.__dict__['cls_attr'] = 'INSTANCE_DICT'

三个键都实打实写进了实例字典,取值结果却是:

k.d         -> DATA_DESC        # 不是 INSTANCE_DICT
k.n         -> INSTANCE_DICT    # 不是 NONDATA_DESC
k.cls_attr  -> INSTANCE_DICT    # 不是 CLASS_ATTR
k.missing   -> GETATTR:missing  # 字典里根本没有

名字一样,命运不同。决定权不在实例字典,而在类那边那个同名对象有没有 __set__。

二、判定标准只有一个:有没有 set

print(hasattr(K.__dict__['d'], '__set__'))   # True  -> 数据描述符
print(hasattr(K.__dict__['n'], '__set__'))   # False -> 非数据描述符

Python 的叫法很直白:

  • 数据描述符(data descriptor):定义了 __set__ 或 __delete__。property 是最常见的一种。
  • 非数据描述符(non-data descriptor):只定义 __get__。普通函数就是这一种,所以 self.method 才会变成绑定方法。

只有数据描述符能压过实例字典。原因也不神秘:它既能读又能写,如果允许实例字典里的同名键把它盖掉,那它的 __set__ 就永远轮不到执行,赋值语义会自相矛盾。

三、完整的五层链

object.__getattribute__ 内部的查找顺序,一句话版:

1. 类及其 MRO 上找同名属性,若是「数据描述符」 -> 直接调它的 __get__,结束
2. 查实例 __dict__ -> 命中就返回,结束
3. 类上那个属性若是「非数据描述符」 -> 调它的 __get__,结束
4. 类上那个属性是普通值 -> 直接返回,结束
5. 以上全没找到,且类定义了 __getattr__ -> 调 __getattr__,否则抛 AttributeError

注意第 1 步在实例字典之前。这就是 k.d 明明字典里有值,却返回 DATA_DESC 的原因。

也注意第 3 步:普通函数是非数据描述符,所以实例属性可以覆盖方法。

class A:
    def f(self): return 'method'
a = A()
a.f = 'shadowed'
print(a.f())        # TypeError: 'str' object is not callable

这不是 bug,是优先级链按设计跑完了。而 property 因为有 __set__(哪怕你没写 setter,property 对象也自带抛 AttributeError 的 __set__),实例属性盖不住它:

class B:
    @property
    def x(self): return 42
b = B()
b.x = 99            # AttributeError: property 'x' of 'B' object has no setter

四、性能账怎么读

把实测数据按链上的位置重排:

路径 命中层 ns/次
k.plain 第 2 层:实例字典 22.1
k.n 第 2 层:实例字典(先否掉非数据描述符) 57.1
k.d 第 1 层:数据描述符 __get__ 137.6
k.missing 第 5 层:__getattr__ 兜底 191.5
g.x(覆写 __getattribute__) Python 层再进一次函数 226.5

三点值得留意:

  1. k.plain 和 k.n 都命中实例字典,却差了 2.6 倍。因为 k.n 得先走一遍类 MRO、判断出 NonDataDesc 不是数据描述符,才能放心去查实例字典。多出来的就是这段判定。
  2. 数据描述符贵在要调 Python 层的 __get__,137.6ns 里绝大部分是一次函数调用。
  3. 别为了「灵活」去覆写 __getattribute__,226.5ns 已经比普通属性慢十倍,而且类里每一个属性访问都要过你这段 Python 代码。真要兜底,用 __getattr__ —— 它只在找不到时才被叫到。

五、调试时怎么看穿这条链

inspect.getattr_static 会绕开描述符协议,把「原始找到的那个对象」还给你,不触发 __get__:

import inspect
print(type(inspect.getattr_static(k, 'd')).__name__)   # DataDesc
print(type(inspect.getattr_static(k, 'n')).__name__)   # str  <- 实例字典里那个

一个实用判断:当你怀疑「这个值到底从哪来的」,先 getattr_static,再看 'x' in obj.__dict__,最后翻 type(obj).__mro__ 里各类的 __dict__。比在 __getattribute__ 里打日志干净得多。

六、写下来就三句话

  • 优先级:数据描述符 > 实例字典 > 非数据描述符 > 类属性 > __getattr__。
  • 分水岭是 __set__:有它就能压过实例字典,没有就得排在后面。
  • 这条链每多走一层,就要多付几十纳秒;热路径上尽量让它停在第 2 层。

我把实验脚本留在 d51-probe.py 里,换台机器、换个 Python 版本可以直接复跑。数值会变,量级和顺序不会。

相关文章
|
7天前
|
JavaScript API iOS开发
DeepSeek Harness下载、安装、配置一篇搞定(2026整理,送常用插件)
DeepSeek Harness(DSH)是DeepSeek于2026年开源的MIT协议AI Agent框架,支持插件化组装模型、工具、沙箱与UI,兼容主流大模型。本文提供免Node.js的桌面端安装包(Win/macOS),含详细图文教程及热门插件推荐。
3181 28
|
6天前
|
Linux 开发工具 iOS开发
Codex保姆级教程(下载+安装+登录+使用,含所有版本,2026最新)
Codex是OpenAI推出的开源AI编程智能体,本地运行,可理解整个代码库,支持自然语言指令改代码、执行命令、跑测试、修Bug,兼容Win/macOS/Linux。(239字)
|
2天前
|
云安全 人工智能 安全
|
7天前
|
iOS开发 Docker MacOS
【2026实测】Docker Desktop下载、安装、汉化、配置教程(附官网安装包)
Docker Desktop 是 Docker 官方推出的桌面容器管理工具,集成引擎、CLI、图形界面与 Kubernetes,支持 Win/macOS/Linux。2026 最新版 4.88.1 强化 ARM 原生运行、镜像加速及资源可视化,附中文汉化与国内镜像配置指南。
1408 5
|
云安全 安全 网络安全
|
8天前
|
安全 算法 定位技术
《高性价比人生指南》完整原版:普通人用最少的资源,过好这一生的底层逻辑~How To Live Better
📖《高性价比人生指南》是一份基于实证、拒绝焦虑的生存手册,倡导以“人生时薪”为视角,重新计算时间、金钱与精力的成本与收益。它不承诺逆袭,只教人算清账:省下的钱若耗掉时间便亏;买体验比买物品更值;睡眠是最高杠杆的健康投资;关系贵在情绪价值而非表面热络;学习重在改变行为而非自我安慰。核心理念:用低成本、高回报的微小行动,换取长期的选择权——不是成为谁,而是掌控自己人生的主动权。
|
20天前
|
人工智能 API 开发工具
DeepSeek Harness 神插件:白嫖 13 家大厂模型
`dsh-our-free-model` 是 DeepSeek Harness 神级插件:零配置、零API Key、零账号,聚合13家大厂免费模型(含DeepSeek V4.1 Flash、Kimi K3等),支持本地网关、用量看板与自动降级,真正实现“白嫖即用”。
3534 53
|
12天前
|
人工智能 JSON Linux
【保姆级】ComfyUI部署教程(下载+安装+配置+工作流,2026最新)
ComfyUI 是一款开源节点式AI生成工具,支持图像、视频、音频及3D内容创作。通过拖拽连线组合模型加载、提示词、采样等节点,构建可复用、易修改的工作流,兼容SD1.5、SDXL、Flux、Qwen Image等主流模型。