python3元类的调用顺序

Posted

技术标签:

【中文标题】python3元类的调用顺序【英文标题】:The call order of python3 metaclass 【发布时间】:2019-04-14 05:18:15 【问题描述】:

当我试图理解metaclass 创建一个类实例的顺序时,我感到很困惑。根据这个图表(source),

我输入以下代码来验证它。

class Meta(type):
    def __call__(self):
        print("Meta __call__")
        super(Meta, self).__call__()

    def __new__(mcs, name, bases, attrs, **kwargs):
        print("Meta __new__")
        return super().__new__(mcs, name, bases, kwargs)

    def __prepare__(msc, name, **kwargs):
        print("Meta __prepare__")
        return 

class SubMeta(Meta):
    def __call__(self):
        print("SubMeta __call__!")
        super().__call__()

    def __new__(mcs, name, bases, attrs, **kwargs):
        print("SubMeta __new__")
        return super().__new__(mcs, name, bases, kwargs)

    def __prepare__(msc, name, **kwargs):
        print("SubMeta __prepare__")
        return Meta.__prepare__(name, kwargs)

class B(metaclass = SubMeta):
    pass

b = B()

但是,结果似乎不是这样。

SubMeta __prepare__
Meta __prepare__
SubMeta __new__
Meta __new__
SubMeta __call__!
Meta __call__

任何帮助将不胜感激。

【问题讨论】:

对于那些试图理解 Python 中的元类的人,请查看***.com/questions/17801344/…。 【参考方案1】:

诀窍,确定

更新 2:基于行为,M0.__call__ 在下面被调用的事实必须是 CPython 源代码中 builtin__build_class 中 this line 的副作用(Python/bltinmodule.c)。

为了定义具有元类的类,我们照常调用元类的__prepare____new____init__。这将创建一个可调用的类(在下面的示例中为Meta),但其内部的PyFunction_GET_CODE 插槽不是指向自己的__call__,而是指向其元类的__call__。因此,如果我们调用Meta()(元类对象),我们就会调用M0.__call__

print("call Meta")
print("Meta returns:", Meta('name', (), ))
print("finished calling Meta")

产生:

call Meta
M0 __call__: mmcls=<class '__main__.Meta'>, args=('name', (), ), kwargs=
Meta __new__: mcs=<class '__main__.Meta'>, name='name', bases=(), attrs=, kwargs=
Meta __init__: mcs=<class '__main__.name'>, name='name', bases=(), attrs=, kwargs=
Meta returns: <class '__main__.name'>
finished calling Meta

换句话说,我们看到Meta 的行为类似于type,但它(相当神奇且没有很好的文档记录)调用M0.__call__。这无疑是由于在类的类型中查找__call__ 而不是在类的实例中(实际上除了我们正在创建的实例之外没有实例)。这其实是一般情况:不符合我们在Meta类型上调用__call__,而Meta的类型是M0

print("type(Meta) =", type(Meta))

打印:

type(Meta) = <class '__main__.M0'>

这解释了它的来源。 (我仍然认为应该在文档中强调这一点,它还应该描述元类类型的约束——这些在_calculate_winner in Lib/types.py 和作为C 代码的in _PyType_CalculateMetaclass in Objects/typeobject.c 中强制执行。)

更新了原始答案

我不知道你的图表来自哪里,但它是错误的。 更新:实际上你可以为你的元类创建一个元类;请参阅jsbueno's answer,我已经更新了下面的示例。新句子/文本以粗体显示,但最后一部分描述了我对明显缺乏文档的困惑。

您现有的元类代码至少有一个错误。最重要的是,它的__prepare__ 需要是一个类方法。另见Using the __call__ method of a metaclass instead of __new__? 和PEP 3115。 而且,要使用元元类,您的元类需要有自己的元类,而不是基类。

Chris's answer 包含正确的定义。但是元类方法参数和类方法参数之间存在一些不幸的不对称,我将在下面说明。

另一件可能有帮助的事情:注意元类__prepare__ 方法在创建B 类的任何实例之前被调用:它在class B 本身被定义时被调用。为了说明这一点,这里有一个更正的元类和类。我还添加了一些插画师。 根据 jsbueno 的回答,我还添加了一个元元类。我找不到关于此的正式 Python 文档,但我更新了下面的输出。

class M0(type):
    def __call__(mmcls, *args, **kwargs):
        print("M0 __call__: mmcls=!r, "
              "args=!r, kwargs=!r".format(mmcls, args, kwargs))
        return super().__call__(*args, **kwargs)

class Meta(type, metaclass=M0):
    def __call__(cls, *args, **kwargs):
        print("Meta __call__: cls=!r, "
              "args=!r, kwargs=!r".format(cls, args, kwargs))
        return super().__call__(*args, **kwargs)

    def __new__(mcs, name, bases, attrs, **kwargs):
        print("Meta __new__: mcs=!r, name=!r, bases=!r, "
              "attrs=!r, kwargs=!r".format(mcs, name, bases, attrs, kwargs))
        return super().__new__(mcs, name, bases, attrs)

    def __init__(mcs, name, bases, attrs, **kwargs):
        print("Meta __init__: mcs=!r, name=!r, bases=!r, "
              "attrs=!r, kwargs=!r".format(mcs, name, bases, attrs, kwargs))
        super().__init__(name, bases, attrs, **kwargs)

    @classmethod
    def __prepare__(cls, name, bases, **kwargs):
        print("Meta __prepare__: name=!r, "
              "bases=!r, kwargs=!r".format(name, bases, kwargs))
        return 

print("about to create class A")
class A(metaclass=Meta): pass
print("finished creating class A")

print("about to create class B")

class B(A, metaclass=Meta, foo=3):
    @staticmethod
    def __new__(cls, *args, **kwargs):
        print("B __new__: cls=!r, "
              "args=!r, kwargs=!r".format(cls, args, kwargs))
        return super().__new__(cls)

    def __init__(self, *args, **kwargs):
        print("B __init__: args=!r, kwargs=!r, ".format(args, kwargs))

print("finished creating class B")

print("about to create instance b = B()")
b = B('hello', bar=7)
print("finished creating instance b")

现在,让我们观察一下当我运行它时会发生什么,然后将每个部分分开:

$ python3.6 meta.py
about to create class A
Meta __prepare__: name='A', bases=(), kwargs=
M0 __call__: mmcls=<class '__main__.Meta'>, args=('A', (), '__module__': '__main__', '__qualname__': 'A'), kwargs=
Meta __new__: mcs=<class '__main__.Meta'>, name='A', bases=(), attrs='__module__': '__main__', '__qualname__': 'A', kwargs=
Meta __init__: mcs=<class '__main__.A'>, name='A', bases=(), attrs='__module__': '__main__', '__qualname__': 'A', kwargs=
finished creating class A

要创建类A 本身,Python 首先调用元类的__prepare__,将类的名称(A)传递给它,基类的列表(一个空元组——它被称为列表但实际上是元组)和任何关键字参数(无)。正如 PEP 3115 所指出的,元类需要返回一个字典或dict-like 对象;这个只返回一个空字典,所以我们在这里很好。

(我不在这里打印cls 本身,但如果你这样做,你会看到它只是&lt;class '__main__.Meta'&gt;。)

接下来,从__prepare__ 获得字典后,Python 首先调用元元__call__,即M0.__call__,将整组参数作为args 元组传递。 然后它用类的所有属性填充__prepare__-supplied 字典,将其作为attrs 传递给元类__new____init__。如果您打印从__prepare__ 返回并传递给__new____init__ 的字典的id,您将看到它们都匹配。

由于类A 没有方法或数据成员,我们在这里只能看到神奇的__module____qualname__ 属性。我们也没有看到关键字参数,所以现在让我们继续创建类B

about to create class B
Meta __prepare__: name='B', bases=(<class '__main__.A'>,), kwargs='foo': 3
M0 __call__: mmcls=<class '__main__.Meta'>, args=('B', (<class '__main__.A'>,), '__module__': '__main__', '__qualname__': 'B', '__new__': <staticmethod object at 0x800ad0a58>, '__init__': <function B.__init__ at 0x800ad2840>, '__classcell__': <cell at 0x800a749d8: empty>), kwargs='foo': 3
Meta __new__: mcs=<class '__main__.Meta'>, name='B', bases=(<class '__main__.A'>,), attrs='__module__': '__main__', '__qualname__': 'B', '__new__': <staticmethod object at 0x800ad0940>, '__init__': <function B.__init__ at 0x800ad27b8>, '__classcell__': <cell at 0x800a745b8: empty>, kwargs='foo': 3
Meta __init__: mcs=<class '__main__.B'>, name='B', bases=(<class '__main__.A'>,), attrs='__module__': '__main__', '__qualname__': 'B', '__new__': <staticmethod object at 0x800ad0940>, '__init__': <function B.__init__ at 0x800ad27b8>, '__classcell__': <cell at 0x800a745b8: Meta object at 0x802047018>, kwargs='foo': 3
finished creating class B

这个比较有趣。现在我们有了一个基类,即__main__.AB 类还定义了几个方法(__new____init__),我们在传递给元类 __new____init__ 方法的 attrs 字典中看到它们(记住,它们只是现在填充的方法)元类的__prepare__ 返回的字典)。 和以前一样,传递是通过元元类M0.__call__ 进行的。我们还看到一个关键字参数,'foo': 3。在属性字典中,我们还可以观察到神奇的 __classcell__ 条目:请参阅 Provide __classcell__ example for Python 3.6 metaclass 以了解有关此内容的简短描述,但是,呃,super-简而言之,它是用于让super() 工作。

关键字参数被传递给所有三个元类方法,以及元元类的方法。 (我不太清楚为什么。请注意,在任何 metaclass 方法中修改字典不会影响任何其他方法,因为它每次都是原始关键字参数的副本。但是,我们可以在元元类中对其进行修改:将kwargs.pop('foo', None) 添加到M0.__call__ 以观察这一点。)

现在我们有了AB 类,我们可以继续创建B 类的实际实例的过程。现在我们看到元类的__call__ 被调用(不是元元类的):

about to create instance b = B()
Meta __call__: cls=<class '__main__.B'>, args=('hello',), kwargs='bar': 7

可以更改传递的argskwargs,但我不会;上面的示例代码最终调用了type.__call__(cls, *args, **kwargs)(通过super().__call__ 的魔力)。这又会调用B.__new__B.__init__

B __new__: cls=<class '__main__.B'>, args=('hello',), kwargs='bar': 7
B __init__: args=('hello',), kwargs='bar': 7, 
finished creating instance b

它完成了类B的新实例的实现,然后我们将其绑定到名称b

注意B.__new__ 说:

return super().__new__(cls)

所以我们调用object.__new__ 来创建实例——这或多或少是所有 Python 版本的要求;当您返回一个单例实例(理想情况下,一个不可修改的实例)时,您只能“作弊”。是 type.__call__ 在这个对象上调用 B.__init__,传递我们传递给它的参数和关键字参数。如果我们将Meta__call__ 替换为:

    def __call__(cls, *args, **kwargs):
        print("Meta __call__: cls=!r, "
              "args=!r, kwargs=!r".format(cls, args, kwargs))
        return object.__new__(cls)

我们将看到B.__new__B.__init__ 从未被调用:

about to create instance b = B()
Meta __call__: cls=<class '__main__.B'>, args=('hello',), kwargs='bar': 7
finished creating instance b

这实际上会创建一个无用/未初始化的实例b。因此元类__call__ 方法调用底层类的__init__ 至关重要,通常是通过super().__call__ 调用type.__call__。如果底层类有一个__new__,元类应该首先调用它,通常再次调用type.__call__

旁注:the documentation 所说的内容

引用第 3.3.3.6 节:

通过执行类主体填充类命名空间后,通过调用metaclass(name, bases, namespace, **kwds) 创建类对象(此处传递的附加关键字与传递给__prepare__ 的关键字相同)。

这解释了在创建b 作为类B 的实例时调用Meta.__call__,但不是Python 在调用Meta.__new__Meta.__init__ 之前在创建类@987654442 时首先调用M0.__call__ 的事实@和B他们自己。

下一段提到了__classcell__ 条目;之后的一个继续描述__set_name____init_subclass__ 钩子的使用。这里没有告诉我们 Python 如何或为什么调用 M0.__call__

之前,在 3.3.3.3 到 3.3.3.5 节中,文档描述了确定元类、准备类命名空间和执行类主体的过程。这是元元类动作应该描述的地方,但不是。

几个附加部分描述了一些附加约束。一个重要的是 3.3.10,它讨论了如何通过对象类型找到特殊方法,绕过常规成员属性查找,甚至(有时)元类 getattribute,说:

以这种方式绕过__getattribute__() 机制为解释器中的速度优化提供了很大的空间,但代价是处理特殊方法时具有一定的灵活性(特殊方法必须设置在类对象本身,以便解释器始终如一地调用)。

更新 2:这确实是诀窍的秘密:通过类型的类型找到特殊的 __call__ 方法。如果元类有元类,则元类提供__call__槽;否则元类的类型是type,所以__call__槽是type.__call__

【讨论】:

尽管你的回答很长,而且关于类创建过程的信息相当有用,但你没有触及“元元类”调用方法的主题,OP 对此表示怀疑。实际上,OP的代码中只有一个简单的错误-问题中发布的图表实际上是正确的。 虽然我不确定我是否同意 jsbueno 关于提问者代码中的错误是什么,但我同意您似乎错过了元元类方面,以及问题中的图表是正确的。 @jsbueno:有趣。我没有在其他地方看到这个描述。事实上,docs.python.org/3/reference/datamodel.html 在描述常规元类如何工作时并没有提及它。 @user2357112:确实,测试表明它有效——但在我能找到的任何文档中似乎都没有提示。 感谢一百万您的详细回答,我学到了很多。该图来自博客 — understanding-python-metaclasses。正如 jsbueno 所提到的,元元类应该class SubMeta(type, metaclass=Meta): 而不是class SubMeta(Meta):【参考方案2】:

尽管@torek 的回答很冗长,但有很多关于类创建的其他细节,但您对这个问题的总结大部分是正确的。

您的代码中唯一的错误可能会让您感到困惑的是,您调用Meta 的类本身必须是来自SubMeta元类,而不是它的父类。

只需将Submeta 声明更改为:

class SubMeta(type, metaclass=Meta):
    ...

(它也不需要从“Meta”继承 - 它只能从 type 派生。否则,尽管考虑对 type.__call__ 进行自定义,同时对创建实例很有用您的课程(即调用SubMeta.__call__ 时)和您的课程本身(调用Meta.__call__))

这是我刚刚在终端输入的另一个更简短的示例。很抱歉命名不一致,并且不够完整 - 但它显示了要点:

class M(type):
    def __call__(mmcls, *args, **kwargs):
        print("M's call", args, kwargs)
        return super().__call__(*args, **kwargs)

class MM(type, metaclass=M):
    def __prepare__(cls, *args, **kw):
        print("MM Prepare")
        return 
    def __new__(mcls, *args, **kw):
        print("MM __new__")
        return super().__new__(mcls, *args, **kw)

class klass(metaclass=MM):
    pass

处理 klass 正文后,Python 输出为:

MM Prepare
M's call ('klass', (), '__module__': '__main__', '__qualname__': 'klass') 
MM __new__

此外

从这里可以看出,使用元元类可以自定义元类__init____new__的调用顺序和参数,但是仍然有一些步骤不能从纯 Python 代码自定义,并且需要对 API 的本地调用(以及可能的原始对象结构操作) - 即:

无法控制对__prepare__的调用 无法控制对已创建类的__init_subclass__ 的调用 可以控制何时调用描述符的__set_name__

最后两项发生在 meta-meta 的 __call__ 返回之后,并且在恢复流到类模块所在的模块之前。

【讨论】:

以上是关于python3元类的调用顺序的主要内容,如果未能解决你的问题,请参考以下文章

爬虫代理池源代码测试-Python3WebSpider

metaclass(元类)

Python3---常见函数---super()

Python3 面向对象之-----元类

python3全栈开发-内置函数补充,反射,元类,__str__,__del__,exec,type,__call__方法

在 python3.x 中显式继承“类型”以实现元类