在元类中使用 __call__ 时 __init__ 错误的回溯行为?

traceback behaviour for __init__ errors when using __call__ in metaclasses?

使用以下代码:

class Meta(type):
    def __new__(mcl, name, bases, nmspc):
        return super(Meta, mcl).__new__(mcl, name, bases, nmspc)

class TestClass(object):
    __metaclass__ = Meta

    def __init__(self):
        pass

t = TestClass(2) # deliberate error

产生以下内容:

Traceback (most recent call last):
  File "foo.py", line 12, in <module>
    t = TestClass(2)
TypeError: __init__() takes exactly 1 argument (2 given)

但是在以下代码中使用 __call__ 而不是 __new__

class Meta(type):
    def __call__(cls, *args, **kwargs):
        instance =  super(Meta, cls).__call__(*args, **kwargs)
        # do something
        return instance

class TestClass(object):
    __metaclass__ = Meta

    def __init__(self):
        pass

t = TestClass(2) # deliberate error

给我以下追溯:

Traceback (most recent call last):
  File "foo.py", line 14, in <module>
    t = TestClass(2)
  File "foo.py", line 4, in __call__
    instance =  super(Meta, cls).__call__(*args, **kwargs)
TypeError: __init__() takes exactly 1 argument (2 given)
  1. type 是否也会从其 __call__ 触发 class 的 __init__,或者当我添加元 class 时行为是否发生了变化?
  2. __new____call__ 都被 运行 调用 class 构造函数。为什么 __call__ 出现在错误消息中而不是 __new__
  3. 有没有办法在此处抑制显示元 class 的 __call__ 的回溯行?即,当错误出现在对构造函数的调用中而不是 __call__ 代码中时?

事实上,解释器抱怨的是你没有将 arg 传递给 __init__

你应该这样做:

t = TestClass('arg')

或:

class TestClass(object):
    __metaclass__ = Meta

    def __init__(self):
        pass

t = TestClass()

看看我能不能回答你的三个问题:

Does type also trigger the __init__ of the class from its __call__ or is the behaviour changed when I add the metaclass?

type.__call__的默认行为是用cls.__new__创建一个新对象(可能继承自object.__new__,或者用super调用它)。如果从 cls.__new__ 编辑的对象 return 是 cls 的一个实例,type.__call__ 将在其上 运行 cls.__init__

如果您在自定义元class 中定义自己的__call__ 方法,它几乎可以做任何事情。通常虽然你会在某个时候调用 type.__call__(通过 super),但同样的行为也会发生。但这不是必需的。您可以 return 元 class 的 __call__ 方法中的任何内容。

Both __new__ and __call__ are being run by the call to the class constructor. Why is __call__ showing up in the error message but not __new__?

您误解了 Meta.__new__ 的用途。当您创建普通 class 的实例时,不会调用 metaclass 中的 __new__ 方法。当您创建 metaclass 的实例时调用它,它是 class 对象本身。

尝试 运行ning 这段代码,以更好地理解发生了什么:

print("Before Meta")

class Meta(type):
    def __new__(meta, name, bases, dct):
        print("In Meta.__new__")
        return super(Meta, meta).__new__(meta, name, bases, dct)

    def __call__(cls, *args, **kwargs):
        print("In Meta.__call__")
        return super(Meta, cls).__call__(*args, **kwargs)

print("After Meta, before Cls")

class Cls(object):
    __metaclass__ = Meta

    def __init__(self):
        print("In Cls.__init__")

print("After Cls, before obj")

obj = Cls()

print("Bottom of file")

您将获得的输出是:

Before Meta
After Meta, before Cls
In Meta.__new__
After Cls, before obj
In Meta.__call__
In Cls.__init__
Bottom of file

请注意,Meta.__new__ 是在定义常规 class Cls 的地方调用的,而不是在创建 Cls 的实例时调用的。 Cls class 对象实际上是 Meta 的一个实例,所以这是有道理的。

您的异常回溯的差异来自于这个事实。当异常发生时,metaclass 的 __new__ 方法早就结束了(因为如果它没有发生,就根本不会有常规的 class 可以调用) .

Is there a way of suppressing the lines of the traceback showing the __call__ for the metaclass here? i.e. when the error is in the call to the constructor and not the __call__ code?

是也不是。这可能是可能的,但它几乎肯定是一个坏主意。默认情况下,Python 的堆栈跟踪将向您显示完整的调用堆栈(不包括用 C 实现的内置内容,而不是 Python)。这就是他们的目的。在你的代码中导致异常的问题并不总是在最后一次调用中,即使在比 metaclasses.

更容易混淆的地方

考虑这个简单的例子:

def a(*args):
    b(args) # note, no * here, so a single tuple will be passed on

def b(*args):
    c(*args):

def c():
    print(x)

a()

在这段代码中,a 函数有一个错误,但只有当 b 调用 c 时参数数量错误才会引发异常。

我想如果你需要的话,你可以通过在某个地方编辑堆栈跟踪对象中的数据来使事情变得更漂亮,但如果你自动这样做,如果你遇到实际的情况,可能会使事情变得更加混乱元class 代码错误。