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
本身,但如果你这样做,你会看到它只是<class '__main__.Meta'>
。)
接下来,从__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__.A
。 B
类还定义了几个方法(__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__
以观察这一点。)
现在我们有了A
和B
类,我们可以继续创建B
类的实际实例的过程。现在我们看到元类的__call__
被调用(不是元元类的):
about to create instance b = B()
Meta __call__: cls=<class '__main__.B'>, args=('hello',), kwargs='bar': 7
可以更改传递的args
或kwargs
,但我不会;上面的示例代码最终调用了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元类的调用顺序的主要内容,如果未能解决你的问题,请参考以下文章
python3全栈开发-内置函数补充,反射,元类,__str__,__del__,exec,type,__call__方法