元类的 __new__ 和 __init__ 参数

Ced*_* H. 5 python metaclass python-3.x

我对方法调用顺序和覆盖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__
Run Code Online (Sandbox Code Playgroud)

输出是:

Name as received in new: A
Name as received in init: A
'AHELLO'
Run Code Online (Sandbox Code Playgroud)

简而言之,我本来预计init会收到AHELLO争论name

我想象这__init__是由super().__new__以下调用的:如果未在覆盖中完成调用,则不会调用__new__my __init__

有人可以澄清__init__在这种情况下如何调用吗?

有关信息,我的用例是我想在特殊情况下通过仅提供一个“基”类(而不是元组)来在运行时更轻松地创建类,然后我添加了以下代码__new__

if not isinstance(bases, tuple):
            bases = (bases, )
Run Code Online (Sandbox Code Playgroud)

但是,我发现我还需要将它添加到__init__.

jsb*_*eno 6

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

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
Run Code Online (Sandbox Code Playgroud)

Python 中的大多数对象实例化都会发生这种情况,包括实例化类本身时 - 元类作为类语句的一部分隐式调用。在这种情况下,__new____init__调用自是元类type.__call__本身的方法。在这种情况下,它充当“元元类”——一个很少需要的概念,但它创造了您正在探索的行为。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)
Run Code Online (Sandbox Code Playgroud)

当然,这是一种比任何人在生产代码中所希望的更接近“海龟一直到底部”的方法。

另一种方法是将修改后的名称保留为元类的属性,以便其__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)
        ...
Run Code Online (Sandbox Code Playgroud)


Kas*_*mvd 5

显然调用了您的__init__方法,其原因是您的__new__方法返回了类的实例。

\n\n

来自https://docs.python.org/3/reference/datamodel.html#object。新的

\n\n
\n

如果__new__()返回 cls 的实例,则将__init__()像 一样调用新的 instance\xe2\x80\x99s 方法__init__(self[, ...]),其中 self 是新实例,其余参数与传递给 的参数相同__new__()

\n
\n\n

正如您所看到的,传递给方法的参数__init__是传递给__new__方法的调用者的参数,而不是当您使用super. 虽然有点含糊,但是如果你仔细阅读的话就是这个意思。

\n\n

至于其余部分,它的工作原理与预期一致:

\n\n
In [10]: A.__bases__\nOut[10]: (list,)\n\nIn [11]: a = A()\n\nIn [12]: a.__class__.__bases__\nOut[12]: (list,)\n
Run Code Online (Sandbox Code Playgroud)\n