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

时间:2015-03-15 01:00:11

标签: python metaclass traceback

使用以下代码:

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是否还会从__init__触发类的__call__,还是在添加元类时更改了行为?
  2. __new____call__都是通过调用类构造函数运行的。为什么__call__会显示在错误消息中而不是__new__
  3. 有没有办法抑制回溯的线条,在这里显示元类的__call__?即错误是在构造函数的调用而不是__call__代码?

2 个答案:

答案 0 :(得分:3)

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

  

type是否还会从__init__触发类的__call__,或者在添加元类时是否更改了行为?

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

如果您在自定义元类中定义自己的__call__方法,它几乎可以执行任何操作。通常,您会在某个时候(通过type.__call__)调用super,因此会发生相同的行为。但这并不是必需的。您可以从元类的__call__方法返回任何内容。

  

__new____call__都是通过调用类构造函数来运行的。为什么__call__会显示在错误消息中而不是__new__

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

尝试运行此代码,以便更好地了解正在发生的事情:

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__在定义常规类Cls时调用,而不是在创建Cls实例时调用。 Cls类对象实际上是Meta的一个实例,所以这是有道理的。

异常回溯的不同来自于这一事实。当异常发生时,元类的__new__方法早已完成(因为如果它没有,那么根本就没有一个常规类可以调用)。

  

有没有办法抑制回溯线显示元类的__call__?即错误是在构造函数的调用中而不是__call__代码?

是和否。它可能是可能的,但它几乎肯定是一个坏主意。默认情况下,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时才会引发异常。

我想如果你需要通过在某处编辑堆栈跟踪对象中的数据来稍微改善一些事情,但如果你自动这样做,如果你遇到实际的错误,可能会让事情变得更加混乱。元类代码。

答案 1 :(得分:-1)

事实上,翻译所抱怨的是你没有将arg传递给__init__

你应该这样做:

t = TestClass('arg')

或:

class TestClass(object):
    __metaclass__ = Meta

    def __init__(self):
        pass

t = TestClass()