Python3元类的调用顺序

20 浏览
0 Comments

Python3元类的调用顺序

我正在尝试理解元类创建类实例的顺序时感到困惑。根据这个图示来源),\"enter

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

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__

任何帮助都将不胜感激。

admin 更改状态以发布 2023年5月24日
0
0 Comments

可行的方法

更新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}

可以更改传递的参数argskwargs,但我不这样做;上面的示例代码最终会调用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在创建类AB本身时首先调用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__

0
0 Comments

尽管@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__的调用
  • \n

  • 无法控制在创建的类上调用__init_subclass__
  • \n

  • 可以控制描述符的__set_name__何时被调用
  • \n

\n后两个项目发生在元元的__call__返回之后,在恢复流向类模块所在模块之前。

0