为什么 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>
如果 A
是 Exception
的子类,它的 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>
请看下面的代码片段:
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>
如果 A
是 Exception
的子类,它的 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>