零参数 super() 依赖于一个名为 __class__ 的编译器生成的闭包单元,这在任何在 Python 类体内定义的方法的闭包中会隐式创建。当编译器处理类定义时,它在方法的闭包中创建一个单元变量 __class__,指向当前正在定义的类对象。当不带参数调用 super() 时,C 实现检查调用帧,找到这个 __class__ 单元,并将其作为第一个参数(类型)。然后,它使用方法的第一个位置参数(通常是 self)作为实例。这个机制在定义时将类引用绑定,而不是在调用时,从而消除了硬编码类名的需要,同时确保继承链中的每个方法引用它在 MRO(方法解析顺序)中的特定位置。
class Base: def method(self): return "Base" class Middle(Base): def method(self): # __class__ 在这里绑定到 Middle return f"Middle -> {super().method()}" class Derived(Middle): def method(self): # __class__ 在这里绑定到 Derived return f"Derived -> {super().method()}"
我们维护了一个具有深层定价模型层次的定量交易库。BaseModel 提供了 calculate_risk() 方法,EquityModel 重写了它以添加针对股票的特定逻辑,而 AmericanOptionModel 则进一步专门化。在一次将 EquityModel 重命名为 VanillaEquityModel 的大重构期间,我们发现混合类中有数十个过时的 super(EquityModel, self) 调用,这些调用是复制粘贴的。这些过时的引用导致 TypeError 或沉默的逻辑错误,即调用了错误的父方法,从而破坏了生产风险计算。
解决方案 1:全局搜索和替换重构。 我们考虑使用自动化工具在 200,000 行的代码库中查找并替换所有硬编码类名的 super() 调用。优点: 不需要架构更改,并且适用于遗留的 Python 2 语法。缺点: 它脆弱且不完整;它错过了动态生成的类、基于字符串的动态方法分配和第三方扩展中的引用。它还违反了 DRY 原则,因为类名在每个方法中都重复。
解决方案 2:普遍采用零参数 super()。 我们将整个代码库迁移为使用不带参数的 super()。优点: 这使得类重命名完全安全,消除了重构期间的人为错误的主要来源,并通过删除冗余噪声显著提高了可读性。它正确处理复杂的协作多重继承模式。缺点: 它需要 Python 3.6+(我们有),而对隐式闭包机制不熟悉的开发人员最初会觉得困惑。它也不能在定义后动态附加到类的函数中使用。
解决方案 3:元类注入类引用。 我们简要考虑使用元类在每个方法中注入 _defining_class 属性。优点: 这使机制变得显式且可检查。缺点: 它增加了显著的复杂性和开销,与标准的 CPython 优化冲突,并重塑了语言编译器已经提供的功能。
我们选择了 解决方案 2。迁移在一个冲刺中完成。结果是下一次涉及类重命名的重构任务的时间减少了 40%,并消除了我们 CI 管道中与过时 super() 引用相关的整个错误类。
当以零参数调用 super() 时,它是如何在物理上定位 __class__ 单元的?
在 CPython 中,super() 的实现(在 Objects/typeobject.c 中)使用 PyEval_GetLocals() 检查调用帧的局部变量和闭包。它特别搜索一个名为 __class__ 的自由变量(单元)。这个单元仅在类体内以词法方式定义函数时由编译器创建(由 CO_OPTIMIZED 标志和类作用域指示)。如果找到该单元,super() 将提取类对象;如果没有,抛出 RuntimeError: super(): __class__ cell not found。零参数形式实际上在编译时被转化为 super(__class__, self),其中 __class__ 是闭合的变量。
如果您尝试在类创建后被分配为类属性的函数中使用零参数 super() 会发生什么?
如果您在类体外定义一个函数,然后将其作为方法赋值(例如,MyClass.method = some_function),在该函数中调用 super() 将引发 RuntimeError。这是因为编译器仅为作为类套件的一部分编译的代码对象创建 __class__ 单元。没有该单元,super() 无法确定层次结构中的哪个类是“当前”类,因为它无法区分函数的定义作用域和后面附加的类。
为什么零参数 super() 不会导致无限递归,当子类方法调用 super(),而父类方法也调用 super()?
之所以有效,是因为 __class__ 指向定义了方法的类,而不是实例的运行时类(type(self))。当 Derived.method() 调用 super(),它发现 __class__ 是 Derived,并委托给 Derived.__mro__ 中的下一个类(例如,Middle)。当达到 Middle.method() 并调用 super() 时,它自己的独特 __class__ 单元包含 Middle,因此它查找 Middle 后面的下一个类(例如,Base)。每个层次结构级别使用自己的定义时类引用,确保 MRO 线性向上遍历一次而不回溯到子类。