DEBUG.WIKI
雨云;给项目一台云服务器
丽萨主机;为跨境业务选好 IP;原生 / 住宅 IP · 按套餐选择;查看 IP →
OnlineSIM:在线接收短信,虚拟号码租用

attrs 自定义 slot 赋值报 descriptor 不适用于当前对象

默认 slots 的 attrs 类自己声明额外 __slots__,可能遗留旧类描述符。把纯内部 slot 放在基类,或显式建模为 attrs 字段;两种方案的序列化和比较语义不同。

类上同时写了默认开启 slots 的 @attrs.define 和自己的 __slots__,构造对象正常,给额外属性赋值却报下面这种错误:

TypeError: descriptor 'private' for 'A' objects doesn't apply to a 'A' object

错误里前后都叫 A,不代表它们是同一个类对象。attrs 为建立 slots 会重建类;手写在原类上的额外 slot 描述符,可能仍绑定原来的类,而新类的 slots 是按 attrs 字段重新生成的。attrs 26.1.0 的正式实现仍需要区分这种写法与继承已有 slots。

纯内部存储可以放在普通基类

如果这个属性不属于 attrs 的数据模型,不希望出现在初始化参数、字段列表、相等比较或 asdict() 中,可以把 slot 声明放到单独的普通基类:

import attrs

class InternalSlots:
    __slots__ = ("private",)

@attrs.define
class Record(InternalSlots):
    public: str = "foo"

record = Record()
record.private = 1

attrs 建立新类时会收集继承来的 slots,基类上的描述符也适用于子类实例。这里的 private 没有默认值,第一次读取前仍需由应用初始化;能写入不代表 attrs 会替它生成初始化代码。

该示例适用于普通可变类。冻结类、自定义 __setattr__、复杂多重继承需要另行检查原来的赋值与布局约束,不能把加一个 slots 基类当成所有继承结构的通用修复。

如果它本来就是字段,就交给 attrs 声明

只是不希望调用方在构造时传入这个值,可以将它建模为字段,并分别决定是否参与比较、显示和序列化。例如:

import attrs

@attrs.define
class Record:
    public: str = "foo"
    private: int = attrs.field(
        default=0, init=False, eq=False, repr=False
    )

这与基类 slot 的语义不同。init=False 不会让字段从 attrs.fields() 消失,eq=False 不会让它从 attrs.asdict() 自动消失,repr=False 也只是控制对象表示。确实不应导出的字段,需要在序列化边界选择字段或配置过滤,而不是根据名字猜测它一定私有。

不要把原来的额外 __slots__ 留着,再假定 slots=False 就能修好:关闭 attrs 自动生成 slots 后,普通字段可能又没有可用的实例存储。是否改用非 slots 类,需要连同原类的 slots 声明一起设计。

修订后检查首次赋值与首次读取、构造函数签名、attrs.fields() 和实际导出结果。若此前已经将内部状态写入外部数据,改字段声明不会替你清除那些记录;需要按应用的数据边界处理。这个故障也不同于装饰后的方法里零参数 super() 绑定旧类,不能靠改 super 调用修复一个 slot 描述符。