元类的__new__和__init__参数

9

针对元类中重载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会在传入name参数的情况下接收到AHELLO
我想象__init__是由super().__new__调用的:如果在重写的__new__中没有进行调用,则我的__init__不会被调用。
有人能澄清一下在这种情况下如何调用__init__吗?
提供的信息是,我希望通过只提供一个“基类”(而不是元组)来使特殊情况下类的创建更加容易,在__new__中添加了以下代码:
if not isinstance(bases, tuple):
            bases = (bases, )

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

2个回答

6
事实上,调用普通类的__new____init__方法的是其元类的__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中的大多数对象实例化,包括在实例化类本身时 - 元类隐式地作为类语句的一部分被调用。 在这种情况下,从type.__call__调用的__new____init__是元类自身的方法。 而在这种情况下,type充当“元元类” - 这个概念很少需要,但它创造了您正在探讨的行为。
当创建类时,type.__new__负责调用类(而不是元类)的__init_subclass__和它的描述符的__set_name__方法 - 因此,“元元类”__call__方法不能控制它。
因此,如果您想要以编程方式修改传递给元类__init__的参数,则“正常”的方法将是具有“元元类”,继承自type并与您的元类本身不同,并覆盖其__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)

当然,这是一种比生产代码中任何人都更接近“一路到底的海龟”的方法。

另一种方法是将修改后的名称作为元类上的属性保留下来,以便其__init__方法可以从那里获取所需的信息,并忽略从其自己的元类__call__调用中传递的名称。信息通道可以是元类实例上的普通属性。嗯——事实上,“元类实例”就是正在创建的类本身——哦,看看——传递给type.__new__的名称已经在其中记录——在__name__属性上。

换句话说,要在元类__new__方法中修改类名并在其自己的__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)
        ...

非常清晰,谢谢!我的关于“name”的例子是简化的,实际上我需要修改“bases”,不过你最后的建议很好。 - Cedric H.

5

很明显,你的__init__方法被调用了,这是因为你的__new__方法返回了一个类的实例。

https://docs.python.org/3/reference/datamodel.html#object.new可以看到:

如果__new__() 返回cls的实例,则新实例的__init__() 方法将被调用,如__init__(self[,...]) ,其中self是新实例,其余参数与传递给__new__() 的相同。

正如你所看到的,传递给__init__的参数是传递给__new__方法的调用者的参数,而不是在使用super时调用它的参数。如果你仔细阅读,会发现这有点含糊不清。

至于其余部分,它的工作方式就像预期的那样:

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

In [11]: a = A()

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

谢谢,非常清晰,我在阅读参考文献时错过了“其余参数与传递给__new __()相同”的部分。 - Cedric H.
如果我可以的话:您可以澄清答案:明确引用部分是指“self”和“instance”,在这种特定情况下,“translates”为“mcs”和“class”。此外,我不确定您最后的示例是否完整,您是否忘记复制/粘贴某些部分? - Cedric H.
1
@CedricH。我认为你在这里提到它就足够了。最后一个例子是基于你的代码编写的,我认为很清楚。 - Mazdak

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接