Python 描述符优先级:实例字典里明明有值,为什么点号读到了另一份

10-01 3阅读

同一个名称可以有两处来源

调试对象时,vars(obj) 已经显示某个字段,obj.field 却返回另一个值,不一定是缓存或赋值失败。点号读取会经过属性查找规则,类上放置的描述符可以参与其中。关键不是哪个值最后写入,而是描述符属于哪一类,以及实例字典在这条查找路径上排在哪里。

下面用 Python 3.12.14 做一个纯内存实验。把代码存成 demo.py,运行 python demo.py 即可,不需要依赖包。我们故意给实例字典塞入两个同名字段,让两种描述符面对完全相同的竞争条件,再分别观察读取、赋值和删除后的结果。

先建立两个行为不同的入口

Hard 定义读取和赋值方法,赋值时主动抛出异常;Soft 只定义读取方法。二者都放在 Sample 的类属性上。对于这里实现了读取方法的对象,前者属于数据描述符,后者属于非数据描述符。即使赋值方法只会拒绝写入,Hard 仍然拥有数据描述符的优先级。

代码中的 obj.__dict__.update 是实验探针,用来直接制造同名内容。它绕过正常的点号赋值路径,所以能够把 hard-shadow 写进字典,但这并不意味着之后点号读取一定会选中它。注意类级访问时的 obj 为 None,例子返回描述符自身,便于后面的身份检查。

Python 描述符优先级:实例字典里明明有值,为什么点号读到了另一份

AI概念示意图:上层入口、实例抽屉与下层入口展示不同读取优先级。图片只说明概念,不是运行截图。

class Hard:
    def __get__(self, obj, owner=None):
        if obj is None:
            return self
        return "hard-descriptor"

    def __set__(self, obj, value):
        raise AttributeError("read only")


class Soft:
    def __get__(self, obj, owner=None):
        if obj is None:
            return self
        return "soft-descriptor"


class Sample:
    hard = Hard()
    soft = Soft()


obj = Sample()
obj.__dict__.update(hard="hard-shadow", soft="soft-shadow")
assert obj.hard == "hard-descriptor"
assert obj.soft == "soft-shadow"
print("first:", obj.hard, obj.soft)
print("stored:", vars(obj)["hard"])

try:
    obj.hard = "new"
except AttributeError as exc:
    assert str(exc) == "read only"
    print("assignment:", str(exc))
else:
    raise AssertionError("assignment should fail")

del obj.__dict__["soft"]
assert obj.soft == "soft-descriptor"
assert Sample.hard is vars(Sample)["hard"]
print("after deletion:", obj.soft)

把输出连回查找顺序

实跑第一行是 first: hard-descriptor soft-shadow。同样存在于实例字典里的两个值,只有 soft-shadow 被普通读取选中。接着 stored: hard-shadow 证明另一份值并没有丢失。数据描述符先于实例字典处理读取,非数据描述符则排在实例字典之后,这个差异足以解释看起来矛盾的观察。

第三行 assignment: read only 来自 Hard 的赋值方法,说明正常赋值走到了受控入口。最后一行 after deletion: soft-descriptor 表明删除实例遮挡项后,Soft 的读取方法重新参与。四行输出要一起看:存在、可读、可写是不同检查,不能由其中一项替代其余两项。

还可以把两个字典字段分别删除再重跑,比较只剩类级入口时的行为。每次只改一个条件,能够分清结果来自遮挡项消失,还是描述符本身改变;不要同时换类定义和实例数据后再猜是哪一步起了作用。

不要把这个顺序扩大成安全保证

本例使用普通对象的默认属性查找,没有重写 __getattribute__。自定义查找、代理对象或某些扩展类型可能需要单独分析。描述符要放在类上才能自动参与这套协议;仅把描述符对象塞进实例字典,不会因为它有读取方法就自动调用。继承场景还要先看类的实际方法解析顺序。

只读入口也不是隔离用户代码的安全边界。直接改内部字典、替换类属性或改变其他内部状态,都可能绕开设计意图。实际排错时,先比较 vars(obj) 中的原始内容与点号读取,再从类字典取得描述符,检查其类型是否定义赋值或删除方法。这样能定位读取规则,而不是反复覆盖那个已经存在的字段。

参考资料

文章版权声明:除非注明,否则均为云鹊BLOG原创文章,转载或复制请以超链接形式并注明出处。