为什么 python 的 Exception 的 repr 跟踪传递给 __init__ 的对象?

Why does python's Exception's repr keep track of passed object's to __init__?

请看下面的代码片段:

In [1]: class A(Exception):
   ...:     def __init__(self, b):
   ...:         self.message = b.message
   ...: 

In [2]: class B:
   ...:     message = "hello"
   ...: 

In [3]: A(B())
Out[3]: __main__.A(<__main__.B at 0x14af96790>)

In [4]: class A:
   ...:     def __init__(self, b):
   ...:         self.message = b.message
   ...: 

In [5]: A(B())
Out[5]: <__main__.A at 0x10445b0a0>

如果 AException 的子类,它的 repr returns 是对 B() 的引用,即使我们只传递 B() 的消息属性。

为什么这是 Exception.repr 中的故意行为?如果可能的话,它在 python 伪代码中是如何工作的,因为 cpython 代码不太可读?

好吧,我想我找到窍门了。 Here's C 源代码,但我将在 Python 中重新实现类似的东西来演示。

除了通常的 __init__(您正在覆盖)之外,Python 还有一个名为 __new__ 的魔术方法。当您将 A 构造为 A(B()) 时,它的作用大致类似于

b = B()
a = A.__new__(A, b)
a.__init__(b)

现在,您已经覆盖了 A.__init__,因此 Exception.__init__ 永远不会被调用。但是 A.__new__ 只是 Exception.__new__(更准确地说,它是 BaseException.__new__,它本质上是我链接的 C 源代码)。而且,根据链接的代码,大致是

class BaseException:
  def __new__(cls, *args):
    obj = object.__new__(cls)
    obj.args = args
    return obj

因此我们明确地将参数元组存储在异常对象上名为 args 的字段中。这是传递给构造函数的 实际 参数元组,即使我们覆盖 __init__。所以 repr 只是引用 self.args 来取回原始参数。

请注意,我在这里有点不准确。如果您在 REPL 中检查 BaseException.__new__,您会发现它仍然是 object.__new__。 C 回调的工作方式不同,并使用了一些我们无法访问的编译器魔法,但基本思想是相同的。

创建 Python 对象时,会调用 class 的 __new__ 方法,然后在新实例上调用 __init__ __new__ 方法 returns(假设它 returned 一个新实例,但有时它不会)。

您重写的 __init__ 方法没有保留对 b 的引用,但您没有重写 __new__,因此您继承了此处定义的 __new__ 方法(CPython source link):

static PyObject *
BaseException_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
    // ...
    if (args) {
        self->args = args;
        Py_INCREF(args);
        return (PyObject *)self;
    }
    // ...
}

我省略了不相关的部分。如您所见,BaseException class 的 __new__ 方法存储了对创建异常时使用的参数元组的引用,因此该元组可用于 __repr__ 方法来打印对用于实例化异常的对象的引用。所以这个元组保留了对原始参数 b 的引用。这与 repr 应该 return Python 代码创建相同状态的对象(如果可以的话)的普遍预期是一致的。

请注意,只有 args,而不是 kwds 具有此行为; __new__ 方法不存储对 kwds 的引用并且 __repr__ 不打印它,因此如果使用关键字参数调用构造函数,我们应该不会看到相同的行为而不是位置参数。事实上,这就是我们观察到的:

>>> A(B())
A(<__main__.B object at 0x7fa8e7a23860>,)
>>> A(b=B())
A()

有点奇怪,因为两个 A 对象应该具有相同的状态,但代码就是这样写的,无论如何。

我们可以通过完成 Ipython 的选项卡来探索差异。

In [230]: a=A(B())
In [231]: a
Out[231]: __main__.A(<__main__.B at 0x7f29cd1e71c0>)

ipython 选项卡完成:

In [234]: a.
             args             a.bin           
             message          a.npy           
             with_traceback() a.t1  
In [234]: a.args
Out[234]: (<__main__.B at 0x7f29cd1e71c0>,)

class没有异常,只有一个属性,message:

In [235]: a1=A1(B())
In [236]: a1
Out[236]: <__main__.A1 at 0x7f29ccc21a60>
In [237]: a1.message
Out[237]: 'hello'

和一个简单的异常:

In [238]: e=Exception(B())
In [239]: e
Out[239]: Exception(<__main__.B at 0x7f29ccd0e790>)
In [240]: e.
             args            
             with_traceback()

并使用 a 作为例外:

In [240]: raise(a)
Traceback (most recent call last):
  File "<ipython-input-240-cf5e5bb7d43e>", line 1, in <module>
    raise(a)
A: <__main__.B object at 0x7f29cd1e71c0>