修补 __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__ 访问 __init_subclass__ 中的变量?
深入理解 Python 中的 __init_subclass__