修补 __init_subclass__

Posted

技术标签:

【中文标题】修补 __init_subclass__【英文标题】:Patching __init_subclass__ 【发布时间】:2021-03-20 03:29:13 【问题描述】:

我无法修补自定义类'__init_subclass__。我认为这与我将修补函数绑定到类的方式有关:

def _patched_initsubclass(cls, **kwargs):
    print(f"CLS from subclassing A: cls")
    super(cls, cls).__init_subclass__(**kwargs)

class A: ...

A.__init_subclass__ = _patched_initsubclass.__get__(A, A)

class B(A): ...  # Output: CLS from subclassing A: <class '__main__.A'>

但是,我知道正确设置的__init_subclass__ 应该有不同的输出:

class F:

    def __init_subclass__(cls, **kwargs):
        print(f"CLS from subclassing F: cls")
        pass

class C(F): ...  # Output: CLS from subclassing F: <class '__main__.C'>

即超类中的cls __init_subclass__ 定义在子类化时应该是子类。我试图通过不同的 SO 帖子和docs 找到绑定 dunder 方法的正确方法,但一直找不到正确的方法。

【问题讨论】:

【参考方案1】:

您对super的使用无效;它应该被传递给它被调用的类的类型(例如它被定义的类)和它被传递的实际类型(它被调用的类),所以super(cls, cls) 是在撒谎;您明确使用描述符协议函数__get__ 将其预绑定到A(在B 上调用它时绕过描述符协议),所以它总是说“我正在从A 调用A " 即使它实际上是在其他东西上调用的。

你想要的并不容易以正确的方式去做; your approach of making it a classmethod (which means it actually gets B, not A, as expected) and calling super(cls, None),仍然是错误的,即使它碰巧在这里起作用。你告诉super 遍历None 对象的MRO 并调用它在MRO 中B 之后找到的第一个__init_subclass__。显然,即使 B 不在 MRO 中(应该是错误 according to the docs:“如果第二个参数是对象,isinstance(obj, type) 必须为真。如果第二个参数是一个类型,issubclass(type2, type) 必须为真。"; c'est la vie),它默默地返回object.__init_subclass__ 并调用它;它之所以有效,只是因为object.__init_subclass__ 不做任何事情,也不反对被调用。

正确执行此操作的唯一方法是为每个要修补的类制作一个新版本的_patched_initsubclass,以便知道它正在修补哪个类。奖励,在执行此操作时,您可以通过将 __class__ 放入新方法的闭包范围内以启用零参数 super() 的方式创建闭包(零参数 super() 魔术是由编译器完成所有在引用__class__super 的类中定义的函数实际上是闭包,而__class__ 在闭包范围内可见)。

一个示例解决方案是:

def make_patched_initsubclass_for(__class__):  # Receive class to patch as __class__ directly
    # Same as before, just defined inside function to get closure scope,
    # and super() is called with no arguments
    def _patched_initsubclass(cls, **kwargs):
        print(f"CLS from subclassing A: cls")
        super().__init_subclass__(**kwargs)    # super() roughly equivalent to super(__class__, cls)

    # Returns a classmethod so it descriptor protocol
    # knows to provide class uniformly, never an instance
    return classmethod(_patched_initsubclass)

class A: ...

A.__init_subclass__ = make_patched_initsubclass_for(A)  # Produces valid closure for binding to A

class B(A): ...  # Output CLS from subclassing A: <class '__main__.B'>

如果您没有将参数命名为make_patched_initsubclass_for__class__(命名为patched_cls 或类似名称),则必须使用super(patched_cls, cls) 而不是super(),但无论哪种方式都可以。

【讨论】:

是的,我的首要任务是正确绑定,我才意识到我对super() 的使用是不正确的后记。感谢您的详细回复。 关于__class__,这是因为我们包含了class cell吗? 我非常感谢您代码中的 cmets,细分使其更加清晰。 @Algebra8: __class__ 是the spec for the new (as in Python 3.0) super 的一部分。规范将其描述为“单元格”(“名为__class__ 的单元格,其中包含定义函数的类对象”)。 “细胞”不是我习惯看到的术语,但是是的,实际上它的实现方式与闭包相同,因此明确使用闭包(因为,根据规范,“对于在类主体之外定义的函数,__class__未定义") 添加什么类定义添加隐式完成相同的最终结果。 @Algebra8:顺便说一句,我明白了为什么 super(cls, None) 在文档说应该出错时没有出错。他们使用None 作为第二个参数的默认标记,因此当您执行super(cls, None) 时,它的行为与super(cls) 完全相同,后者似乎返回一个未绑定的super 对象,您可以查找@987654370 @ on(它得到super自己的空实现,没什么用处)。我从来不需要未绑定的super,在这种情况下,apparently it almost never is 肯定没有帮助。【参考方案2】:

我找到了一个不涉及通过__get__绑定路径函数的解决方案:

def _patched_initsubclass(cls, **kwargs):
    print(f"CLS from subclassing A: cls")

    super(cls, None).__init_subclass__(**kwargs)


class A: ...


A.__init_subclass__ = classmethod(_patched_initsubclass)


class B(A): ...  # Output CLS from subclassing A: <class '__main__.B'>

我仍然不清楚为什么会这样:即classmethod() 和直接与__get__ 绑定有什么区别。

答案可能与 classmethod 在幕后所做的有关,所以我会调查一下。

我会将这个答案留给其他可能觉得有帮助的人,并将包括任何后续信息。

【讨论】:

需要明确的是,您所做的一切都是无效的,并且仅在调用超类的 __init_subclass__ 时即使正确完成也没有做任何事情。我的答案有正确的方法(以可重用的方式,因此您可以以相同的方式修补多个类,而无需为每个类手写_patched_initsubclass)。 哦,我没有看到你的答案。感谢分享 我将把这个答案留在这里,以便您在回答中所说的内容对任何查看它的人都有意义。

以上是关于修补 __init_subclass__的主要内容,如果未能解决你的问题,请参考以下文章

类装饰器禁用 __init_subclass__

如何从 __init__ 访问 __init_subclass__ 中的变量?

动态 __init_subclass__ 方法的参数绑定

深入理解 Python 中的 __init_subclass__

具有 __init_subclass__ 和子类注册表的注册表模式

元类的“__init_subclass__”方法在此元类构造的类中不起作用