Python3元类的调用顺序
Python3元类的调用顺序
我正在尝试理解元类创建类实例的顺序时感到困惑。根据这个图示(来源),
我键入以下代码来验证它。
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__
任何帮助都将不胜感激。
可行的方法
更新2:基于行为,下面调用了M0.__call__
,肯定是因为在CPython源码的builtin__build_class
函数(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=, args=('name', (), {}), kwargs={} Meta __new__: mcs= , name='name', bases=(), attrs={}, kwargs={} Meta __init__: mcs= , name='name', bases=(), attrs={}, kwargs={} Meta returns: finished calling Meta
换句话说,我们可以看到,Meta
的行为类似于type
,但它(神奇地、缺乏非常好的文档)调用了M0.__call__
。这毫无疑问是因为在类的类型中查找__call__
,而不是在类的实例中查找(事实上,除了我们正在创建的这个实例之外,根本就没有实例)。实际上,这是一般情况:它是基于在Meta
的类型上调用__call__
,而Meta
的类型是M0
,所以会发生这种情况:
print("type(Meta) =", type(Meta))
输出结果:
type(Meta) =
这就解释了这个问题的来源。(我仍然认为这应该在文档中强调,并且文档也应该描述元类类型的约束 - 这些是在Lib/types.py
中的_calculate_winner
和C代码中在Objects/typeobject.c
的_PyType_CalculateMetaclass
中实施的。)
更新原始答案
我不知道你的图表来自何处,但它是错误的。 更新:实际上你可以为你的元类创建一个元类,请参见jsbueno的回答。我已经更新了下面的示例。新的句子/文本加粗,除了最后一节中描述的我对显然缺乏文档的困惑。
你现有的元类代码至少有一个错误。 最重要的是,它的__prepare__
需要是一个类方法。此外,还可以参见使用元类的__call__方法而不是__new__?和PEP 3115。 使用元-元-类,您的元类需要拥有自己的元类,而不是一个基类。
Chris's answer 包含了正确的定义。但是,在元类方法参数和类方法参数之间存在一些不幸的不对称性,我将在下面进行说明。
还有一件可能有帮助的事情是,请注意,在创建任何类B
实例之前,元类__prepare__
方法被调用:当定义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=, args=('A', (), {'__module__': '__main__', '__qualname__': 'A'}), kwargs={} Meta __new__: mcs= , name='A', bases=(), attrs={'__module__': '__main__', '__qualname__': 'A'}, kwargs={} Meta __init__: mcs= , name='A', bases=(), attrs={'__module__': '__main__', '__qualname__': 'A'}, kwargs={} finished creating class A
为了创建类A
本身,Python首先调用元类的__prepare__
,传递类的名称(A
),基类列表(一个空元组 - 它被称为列表,实际上是元组),以及任何关键字参数(无)。正如PEP 3115所述,元类需要返回一个字典或类似于dict
的对象;这个返回一个空字典,所以我们好了。
(我没有在这里打印cls
本身,但是如果您这样做,您将看到它只是
。)
接下来,从__prepare__
获得了一个字典后,Python首先调用元元__call__
,即M0.__call__
,将整个参数集作为args
元组传递。然后,它使用__prepare__
提供的字典来填充类的所有属性,将其作为attrs
传递给元类__new__
和__init__
。如果打印从__prepare__
返回并传递给__new__
和__init__
的字典的id
,您将看到它们全部匹配。
既然A
类没有方法或数据成员,我们只能看到神奇的__module__
和__qualname__
属性。我们也没有关键字参数,所以现在让我们继续创建类B
:
about to create class B Meta __prepare__: name='B', bases=(,), kwargs={'foo': 3} M0 __call__: mmcls= , args=('B', ( ,), {'__module__': '__main__', '__qualname__': 'B', '__new__': , '__init__': , '__classcell__': }), kwargs={'foo': 3} Meta __new__: mcs= | , name='B', bases=( ,), attrs={'__module__': '__main__', '__qualname__': 'B', '__new__': , '__init__': , '__classcell__': }, kwargs={'foo': 3} Meta __init__: mcs= | , name='B', bases=( ,), attrs={'__module__': '__main__', '__qualname__': 'B', '__new__': , '__init__': , '__classcell__': }, kwargs={'foo': 3} finished creating class B |
这个类比较有趣。现在我们有一个基类,即__main__.A
。类B
还定义了几个方法(__new__
和__init__
),我们可以在传递给元类__new__
和__init__
方法的attrs
字典中看到它们(请记住,这只是元类的__prepare__
返回的现在填充的字典)。与以前一样,传递是通过元元类M0.__call__
进行的。在属性字典中,我们还可以观察到神奇的__classcell__
条目:请参见提供Python 3.6元类__classcell__示例,了解它是关于什么的简短说明,但是要超级简短,它是用于使super()
工作。
关键字参数传递给所有三个元类方法,以及元元类的方法。(我不太确定为什么。请注意,在任何元类方法中修改字典不会影响其他方法,因为每次都是原始关键字参数的副本。但是,我们可以在元元类中进行修改:在 M0.__call__中添加
)kwargs.pop('foo', None)
来观察。
现在我们有了类A
和类B
,我们可以继续创建类B
的实例的过程。现在我们看到元类的__call__
被调用(而不是元元类的):
about to create instance b = B() Meta __call__: cls=, args=('hello',), kwargs={'bar': 7}
可以更改传递的参数args
或kwargs
,但我不这样做;上面的示例代码最终会调用type.__call__(cls, *args, **kwargs)
(通过super().__call__
的魔法)。这将调用B.__new__
和B.__init__
:
B __new__: cls=, 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=, args=('hello',), kwargs={'bar': 7} finished creating instance b
这实际上将创建一个无用/未初始化的实例b
。因此,元类__call__
方法调用底层类的__init__
非常重要,通常通过调用type.__call__
通过super().__call__
实现。如果底层类具有__new__
,则元类应首先调用它,通常是通过调用type.__call__
实现。
顺带说一句:文档中提到的内容
引用第3.3.3.6节:
一旦类命名空间通过执行类体被填充,就通过调用metaclass(name, bases, namespace, **kwds)
创建类对象(传递给这里的其他关键字与传递给__prepare__
的相同)。
这解释了在创建类B
的实例b
时调用Meta.__call__
的原因,但是不解释Python在创建类A
和B
本身时首先调用M0.__call__
然后才调用Meta.__new__
和Meta.__init__
的事实。下一个段落提到了__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__
。
尽管@torek的答案非常详细,包括诸多关于类创建的细节,但是你提出的问题大部分是正确的。\n你代码中唯一的问题可能是有些迷惑你的,即你调用的类Meta
必须是SubMeta
的元类本身,而不是它的父类。\n只需将Submeta
声明更改为:\n
class SubMeta(type, metaclass=Meta): ...
\n(无需它也继承自“Meta”-它只能从type
派生。但是,要想在创建类的实例时(即调用SubMeta.__call__
时)和你的类本身(调用Meta.__call__
时)同时具有用处的 type.__call__
自定义,否则无法进行自定义)\n这里是我刚刚在终端上键入的另一个更简短的示例。抱歉为命名不一致而造成的困扰,也很抱歉不是很完整,但是它显示了主要内容:\n
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
\n在处理klass
body时,Python输出如下:\n
MM Prepare M's call ('klass', (), {'__module__': '__main__', '__qualname__': 'klass'}) {} MM __new__
\n
此外
\n正如你从中可以看到的那样,使用元元类可以自定义元类__init__
和__new__
的调用顺序和参数,但仍然有一些步骤无法从纯Python代码中自定义,并且需要调用API(可能是对象结构操作)-这些步骤包括:\n
- \n
- 无法控制对
__prepare__
的调用 - 无法控制在创建的类上调用
__init_subclass__
- 可以控制描述符的
__set_name__
何时被调用
\n
\n
\n
\n后两个项目发生在元元的__call__
返回之后,在恢复流向类模块所在模块之前。