attrs 上下文管理器里 super() 报错:检查 slots 重建的类绑定
attrs 的 slots 类在 contextmanager 包装后,零参数 super 可能保留旧类绑定。使用明确的起始类,保留 MRO、资源清理和再次继承的边界。
给继承方法加上 @contextlib.contextmanager 后,进入 with 才报:
TypeError: super(type, obj): obj must be an instance or subtype of type
如果类同时使用默认开启 slots 的 @attrs.define,而出错行是方法里的零参数 super(),先查类重建与装饰器闭包的组合。对象看起来确实属于这个类,也不能排除这条路径。attrs 26.1.0 的正式文档仍保留这项限制,不能把单纯升级当作已经解决。
装饰后的生成器还记着旧类
下面是会涉及该限制的结构:父类与子类都用 @attrs.define,两边的 hello() 都是上下文管理器;子类在生成器内部调用 super().hello()。
为了添加 slots,attrs 会创建一个新类,替代类语句最初建立的对象。零参数 super() 依赖方法中的 __class__ 闭包。attrs 会修正它能找到的这类引用,但 contextmanager 包装后的函数又保存着原生成器函数,不能据此认为内部每一层闭包都已经改指新类。
调用 child.hello() 可能尚未执行生成器正文,真正进入 with child.hello(): 才触发异常。不要因为构造实例、取到上下文管理器都成功,就认定父类调用正常。
在这个方法中明确起始类
保留 slots 与上下文管理器时,可以把子类里的调用写成双参数形式。下面是独立的结构示例:
import contextlib
import attrs
@attrs.define
class Parent:
@contextlib.contextmanager
def hello(self):
yield None
@attrs.define
class Child(Parent):
@contextlib.contextmanager
def hello(self):
with super(Child, self).hello():
yield None
Child 要写成定义这个覆盖方法的类。不要替换为 type(self):方法再被孙类继承时,以运行时类型作为起点可能重新找到当前方法,造成递归。双参数 super 仍按 MRO 寻找后续实现,不能随手改成固定调用某一个父类,绕过项目原来的多重继承协作关系。
这个示例没有业务资源;实际方法中的 try/finally、异常传播和释放顺序都应保留。仅调整 super 的起点,不要为消除异常删掉父类上下文,也不要在 yield 外吞掉业务错误。
如果项目没有 slots 的需求,也可评估在相关类上使用 @attrs.define(slots=False),避免该类的这次重建。但这会改变实例存储方式与其他 slots 相关行为,不能作为整套模型的无差别替换。父类、子类的组合也要一起核对。
验证进入、退出和再次继承
检查要覆盖真正进入上下文、正常退出以及上下文内抛出异常。确认父类和子类的清理动作仍按预期执行,异常仍交给原调用方;只有“对象能创建”不够。
若还有孙类继承该方法,保留这个分支,检查 MRO 中各层只执行一次。也应检查其他包装后的方法,不能因这一处改成双参数就断言所有装饰器都兼容。
没有 attrs slots、没有包住方法的装饰器,或报错发生在另一个显式 super 调用上时,应继续核对传入的类与对象。这条限制不能解释所有相同文本的 TypeError。


