为什么异常代理 __str__ 到 args 上?

Why does Exception proxy __str__ onto the args?

为什么打印异常实例打印的是exc.args的值,而不是直接表示exc?文档称其为 convenience,但在实践中实际上是 不便

无法区分 *args 和元组:

>>> print(Exception(123, 456))
(123, 456)
>>> print(Exception((123, 456)))
(123, 456)

无法可靠地辨别类型:

>>> print(Exception('123'))
123
>>> print(Exception(123))
123

还有可爱的"invisible"例外:

>>> print(Exception())

>>> 

除非您特别要求不要继承,否则您将继承:

>>> class MyError(Exception):
...     """an error in MyLibrary"""
...     
>>> print(MyError())

>>> 

如果您忘记专门使用 repr 记录错误实例,这可能是一个真正的问题 - 日志文件中的默认字符串表示已不可逆转地丢失信息。

Exception.__str__ 如此奇怪的实现的基本原理是什么?大概如果用户想要打印 exc.args 那么他们应该只打印 exc.args?

BaseException.__str__ 可以用 Python 3 以向后不兼容的方式修复,以至少包括异常的类型,但也许没有人注意到这是应该的固定的。

当前的实现可以追溯到 PEP 0352,它提供了基本原理:

No restriction is placed upon what may be passed in for args for backwards-compatibility reasons. In practice, though, only a single string argument should be used. This keeps the string representation of the exception to be a useful message about the exception that is human-readable; this is why the __str__ method special-cases on length-1 args value. Including programmatic information (e.g., an error code number) should be stored as a separate attribute in a subclass.

当然 Python 本身在很多情况下都打破了有用的人类可读消息的原则 - 例如 KeyError 的字符串化是未找到的键,这会导致调试消息,如

An error occurred: 42

str(e) 本质上是 str(e.args)str(e.args[0]) 的原因最初是为了向后兼容 Python 1.0。在 Python 1.0 中,引发异常的语法,例如 ValueError 应该是:

>>> raise ValueError, 'x must be positive'
Traceback (innermost last):
  File "<stdin>", line 1
ValueError: x must be positive

Python 保留了与 1.0 到 2.7 的向后兼容性,因此您可以 运行 大多数 Python 1.0 程序在 Python 2.7 中保持不变(就像你永远不会做的那样):

>>> raise ValueError, 'x must be positive'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: x must be positive

同样,在 Python 1.0 中,您会用

捕获 ValueError
>>> try:
...     raise ValueError, 'foo'
... except ValueError, e:
...     print 'Got ValueError', e

在 Python 2.7 中没有变化。

但是其内部工作机制发生了变化:在 Python 1.0.1 中,ValueError 是一个 string 值... 'ValueError'

>>> ValueError, type(ValueError)
('ValueError', <type 'string'>)

根本没有例外 class,你只能 raise 一个参数,或者一个元组,用一个字符串作为鉴别符:

>>> class MyCustomException: 
...     pass
...   
>>> raise MyCustomException, 'my custom exception'
Traceback (innermost last):
  File "<stdin>", line 1
TypeError: exceptions must be strings

也可以给出一个元组作为参数:

>>> raise ValueError, ('invalid value for x', 42)
Traceback (innermost last):
  File "<stdin>", line 1
ValueError: ('invalid value for x', 42)

如果你在 Python 1.0 中捕获这个 "exception",你在 e 中得到的是:

>>> try:
...     raise ValueError, ('invalid value for x', 42)
... except ValueError, e:
...     print e, type(e)
... 
('invalid value for x', 42) 42 <type 'tuple'>

一个元组!

让我们试试Python中的代码 2.7:

>>> try:
...     raise ValueError, ('invalid value for x', 42)
... except ValueError, e:
...     print e, e[1], type(e)
... 
('invalid value for x', 42) 42 <type 'exceptions.ValueError'>

除了值的类型外,输出看起来完全相同;之前是 tuple 现在是例外... Exception 不仅将 __str__ 委托给 args 成员,而且它还支持像元组一样的索引 -以及解包、迭代等:

Python 2.7

>>> a, b, c = ValueError(1, 2, 3)
>>> print a, b, c
1 2 3

所有这些技巧都是为了保持向后兼容性。

Python 2.7 行为来自 BaseException class PEP 0352; PEP 0352 最初是在 Python 2.5.

中实现的

在 Python 3 中,删除了旧语法 - 您无法使用 raise discriminator, (arg, um, ents) 引发异常; except 只能使用 Exception as e 语法。

PEP 0352 讨论了放弃对 BaseException:

的多个参数的支持

It was decided that it would be better to deprecate the message attribute in Python 2.6 (and remove it in Python 2.7 and Python 3.0) and consider a more long-term transition strategy in Python 3.0 to remove multiple-argument support in BaseException in preference of accepting only a single argument. Thus the introduction of message and the original deprecation of args has been retracted.

似乎对 args 的弃用被遗忘了,因为它在 Python 3.7 中仍然存在,并且是 唯一 访问给定参数的方式许多内置异常。同样,__str__ 不再需要委托给 args,并且实际上可以为 BaseException.__repr__ 添加别名,从而提供更好、更明确的表示:

>>> BaseException.__str__(ValueError('foo', 'bar', 'baz'))
"('foo', 'bar', 'baz')"
>>> BaseException.__repr__(ValueError('foo', 'bar', 'baz'))
"ValueError('foo', 'bar', 'baz')"

但没有人考虑过。


P.S。异常的 repr 很有用 - 下次尝试使用 !r 格式打印异常:

print(f'Oops, I got a {e!r}')

结果是

ZeroDivisionError('division by zero',)

输出中