为什么将异常代理__str__放入args?

时间:2018-10-26 18:52:46

标签: python exception python-internals

为什么打印异常实例会打印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

还有可爱的“隐形”异常:

>>> print(Exception())

>>> 

除非您明确要求不这样做,否则您将继承:

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

>>> 

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

Exception.__str__ 这样奇怪的实现有什么理由?如果用户想打印exc.args,那么他们应该只打印exc.args吗?

1 个答案:

答案 0 :(得分:10)

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

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

  

出于向后兼容的原因,对于args可以传入的内容没有任何限制。但是,实际上,仅应使用单个字符串参数。这样可以使异常的字符串表示形式成为有关人类可读的异常的有用消息。这就是__str__方法在length-1 args值上特例的原因。包含程序化信息(例如,错误代码号)的信息应作为单独的属性存储在子类中。

当然,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 2.7 中以不变的方式运行大多数Python 1.0程序(就像您永远不会那样):

>>> 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'>)

根本没有异常类,您只能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 中捕获了此“例外”,则您在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行为来自PEP 0352中引入的BaseException类; PEP 0352最初是在Python 2.5中实现的。


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

PEP 0352讨论了有关放弃对BaseException的多个参数的支持:

  

已决定最好弃用Python 2.6中的message属性(并在Python 2.7和Python 3.0中将其删除),并考虑在Python 3.0中使用更长期的过渡策略来删除多个BaseException中的参数支持优先于仅接受单个参数。因此,撤消了对消息的引入和对args的原始弃用。

看来args的这种弃用被遗忘了,因为它在Python 3.7中仍然存在,并且是访问赋予许多内置异常的参数的 only 方法。同样,__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',)

正在输出。