元类的 __new__ 和 __init__ 参数

Arguments of __new__ and __init__ for metaclasses

在 metaclass 中覆盖 newinit 时,我对方法调用顺序和不同的参数感到有点惊讶。考虑以下因素:

class AT(type):
    def __new__(mcs, name, bases, dct):
        print(f"Name as received in new: {name}")
        return super().__new__(mcs, name + 'HELLO', bases + (list,), dct)

    def __init__(cls, name, bases, dct):
        print(f"Name as received in init: {name}")
        pass

class A(metaclass=AT):
    pass

A.__name__

输出为:

Name as received in new: A
Name as received in init: A
'AHELLO'

简而言之,我本以为 init 会收到带有参数 nameAHELLO

我想象 __init__super().__new__ 调用:如果调用没有在覆盖的 __new__ 中完成,那么我的 __init__ 就不会被调用。

谁能解释一下 __init__ 在这种情况下是如何调用的?

有关我的用例的信息,我想创建 classes,在特殊情况下,通过仅提供一个 "base" class 在运行时更容易(而不是元组),然后我在 __new__:

中添加了这段代码
if not isinstance(bases, tuple):
            bases = (bases, )

然而,我发现我还需要在__init__中添加它。

你的 __init__ 方法显然被调用了,原因是你的 __new__ 方法返回了你的 class.

的实例

来自 https://docs.python.org/3/reference/datamodel.html#object.new:

If __new__() returns an instance of cls, then the new instance’s __init__() method will be invoked like __init__(self[, ...]), where self is the new instance and the remaining arguments are the same as were passed to __new__().

如您所见,传递给 __init__ 的参数是传递给 __new__ 方法调用者的参数,而不是当您使用 super 调用它时。有点模糊,但仔细阅读就是这个意思。

关于其余部分,它按预期工作:

In [10]: A.__bases__
Out[10]: (list,)

In [11]: a = A()

In [12]: a.__class__.__bases__
Out[12]: (list,)

事实是,协调调用普通 class 的 __new____init__ 的是其元 class 上的 __call__ 方法。默认元类型 type__call__ 方法中的代码是用 C 编写的,但在 Python 中的等效代码是:

class type:
    ...
    def __call__(cls, *args, **kw):
         instance = cls.__new__(cls, *args, **kw)  # __new__ is actually a static method - cls has to be passed explicitly
         if isinstance(instance, cls):
               instance.__init__(*args, **kw)
         return instance

Python 中的大多数对象实例化都会发生这种情况,包括实例化 classes 本身时 - metaclass 作为 class 语句的一部分被隐式调用.在这种情况下,从 type.__call__ 调用的 __new____init__metaclass 本身的方法。在这种情况下,type 充当 "metametaclass" - 一个很少需要的概念,但它是创建您正在探索的行为的原因。

创建 classes 时,type.__new__ 将负责调用 class(不是元class)__init_subclass__,及其描述符__set_name__ 方法 - 因此,"metametaclass" __call__ 方法无法控制。

因此,如果您希望以编程方式修改传递给 metaclass __init__ 的参数,"normal" 方法将有一个 "metametaclass",继承来自 type 并与您的元 class 本身不同,并覆盖其 __call__ 方法:

class MM(type):
    def __call__(metacls, name, bases, namespace, **kw):
        name = modify(name)
        cls = metacls.__new__(metacls, name, bases, namespace, **kw)
        metacls.__init__(cls, name, bases, namespace, **kw)
        return cls
        # or you could delegate to type.__call__, replacing the above with just
        # return super().__call__(modify(name), bases, namespace, **kw)

当然,这是一种比生产代码更接近 "turtles all way to the bottom" 的方法。

另一种方法是将修改后的名称保留为元class 上的一个属性,以便其 __init__ 方法可以从那里获取所需的信息,并忽略从其传入的名称自己的 metaclass' __call__ 调用。信息通道可以是 metaclass 实例上的一个普通属性。好吧 - 碰巧 "metaclass instance" 是 class 自己创建的 - 哦,看 - 传递给 type.__new__ 的名称已经记录在其中 - 在 __name__属性.

换句话说,要在 metaclass __new__ 方法中使用修改过的 class 名称,您需要做的就是在它自己的 __init__ 方法中使用忽略传入的 name 参数,并使用 cls.__name__ 代替:

class Meta(type):
    def __new__(mcls, name, bases, namespace, **kw):
        name = modified(name)
        return super().__new__(mcls, name, bases, namespace, **kw)

    def __init__(cls, name, bases, namespace, **kw):
        name = cls.__name__  # noQA  (otherwise linting tools would warn on the overriden parameter name)
        ...