为啥`type.__new__`调用`__init_subclass__`?

Posted

技术标签:

【中文标题】为啥`type.__new__`调用`__init_subclass__`?【英文标题】:Why does `type.__new__` call `__init_subclass__`?为什么`type.__new__`调用`__init_subclass__`? 【发布时间】:2021-06-03 10:38:48 【问题描述】:

我注意到我无法以我想要的方式将__init_subclass__ 与 Django 模型类一起使用。在父类的__init_subclass__ 方法运行时,元类似乎还没有完成创建子类。虽然我了解问题所在,并且可以通过制作自定义元类来规避它,但我不明白的是为什么

在我的脑海中,我倾向于认为任何像 __new__ 这样的调用都应该在像 __init__ 这样的调用发生之前完成。但元类和__init_subclass__ 并非如此,如下所示:

class MetaMeta(type):
    print('parsing MetaMeta')
    def __call__(cls, *args, **kwargs):
        print('entering MetaMeta.__call__')
        instance = super().__call__(*args, **kwargs)
        print('leaving MetaMeta.__call__')
        return instance

class Meta(type, metaclass=MetaMeta):
    print('parsing Meta')
    def __init__(self, *args, **kwargs):
        print('  entering Meta.__init__')
        super().__init__(*args, **kwargs)
        print('  leaving Meta.__init__')
    def __new__(cls, *args, **kwargs):
        print(f'  entering Meta.__new__')
        instance = super().__new__(cls, *args, **kwargs)
        print('  leaving Meta.__new__')
        return instance

class Parent(object, metaclass=Meta):
    print('parsing Parent')
    def __init_subclass__(cls, *args, **kwargs):
        print('    entering Parent.__init_subclass__')
        super().__init_subclass__(*args, **kwargs)
        print('    leaving Parent.__init_subclass__')

class Child(Parent):
    print('parsing Child')

结果:

parsing MetaMeta
parsing Meta
parsing Parent
entering MetaMeta.__call__
  entering Meta.__new__
  leaving Meta.__new__
  entering Meta.__init__
  leaving Meta.__init__
leaving MetaMeta.__call__
parsing Child
entering MetaMeta.__call__
  entering Meta.__new__
    entering Parent.__init_subclass__
    leaving Parent.__init_subclass__
  leaving Meta.__new__
  entering Meta.__init__
  leaving Meta.__init__
leaving MetaMeta.__call__

在调用__init_subclass__ 之后,元类仍然可以在Meta.__new__ 中设置类。这对我来说似乎很奇怪。为什么会这样,有没有办法在Parent(没有自定义元类)中提供完全在Meta.__new__ 之后(可能在Meta.__init__ 之前)运行的代码?

或者我完全错过了什么?

仅供参考,我找到了一些相关的主题,但不是我想要的:

The call order of python3 metaclass Arguments of __new__ and __init__ for metaclasses https://docs.python.org/3/reference/datamodel.html#customizing-class-creation

也许问这个问题的更简洁的方法是“为什么 Python(至少是 v3.9)有Meta.__new__ 调用Parent.__init_subclass__,而不是让MetaMeta.__call____new__ 完成后立即调用它?

请注意,在询问之后,我确实找到了一些关于这个主题的 python.org 讨论,但我认为他们没有说明原因:

https://bugs.python.org/issue42775 https://mail.python.org/archives/list/python-dev@python.org/thread/ZMRRNSFSLJZDGGZ66CFCYQBINU62CDNX/

【问题讨论】:

【参考方案1】:

棘手。

所以,就是这样,因为它是用语言制作的。在为类创建过程添加功能时,人们不允许自定义何时 __init_subclass__,或描述符 __set_name__,或计算最终线性化 (mro):所有这些都在 type.__new__ 内一次完成 - 和任何编写的元类都必须在某个时候调用type.__new__

但是,有一个可能的解决方法:如果 type.__new__ 不会“看到”__init_subclass__ 方法,则不会调用它。

因此,子元类可以隐藏__init_subclass__,调用父类__new__,然后在离开自己的__new__ 之前恢复并调用__init_subclass__

所以,如果问题特别是在 Django 的元类 __new__ 完成后需要 __init_subclass__ 运行,我可以想到两个选项,它们都涉及从 Django 的 ORM 元类继承、修改它并使用它作为模型的元类。

那么第一个选项就是在你的项目中使用另一个方法名而不是__init_subclass__。你的自定义元类调用 super().__new__() ,Django 和 Python 做他们的事情,你调用你的 __init_subclass2__ (或者你选择的任何名字)。 我认为这是最易于维护和最直接的方式。

第二个选项是我之前提到的:您的__new__ 方法检查所有基是否出现__init_subclass__,然后从它们所在的类中临时删除,存储原始方法,调用super().__new__(),然后恢复__init_subclass__ 方法,然后调用它们。这样做的好处是可以在层次结构中使用现有的__init_subclass__(只要类本身是用 Python 而不是本机代码编写的:在这种情况下暂时删除该方法将不起作用)。它的严重缺点是您必须自己扫描 mro(您必须重新进行线性化)搜索所有现有的 __init_subclass__ 并在之后恢复它们 - 可能存在一些难以发现的极端情况。

【讨论】:

以上是关于为啥`type.__new__`调用`__init_subclass__`?的主要内容,如果未能解决你的问题,请参考以下文章

自定义元类 __call__,__init__,__new__总结

原创Python 对象创建过程中元类, __new__, __call__, __init__ 的处理

__init__和__new__

异常处理

默认类型 .__call__ 是不是比调用 __new__ 和 __init__ 更多?

python中的内方法