为什么如果元组的第一个元素是异常,则提升元组会起作用?

24
我很难理解这个问题,它与在Python 2.7中引发异常时可能出现的错误有关:
try:
  raise [1, 2, 3, 4]
except Exception as ex:
  print ex

这里的信息是“异常必须是旧式类或派生自BaseException,而不是列表”- 这部分没问题,但当我将它更改为元组时,我感到困惑。
try:
  raise (1, 2, 3, 4)
except Exception as ex:
  print ex

这里的错误信息是“异常必须是旧式类或继承自BaseException,而不是int”- 为什么会被解释为引发了一个整数而不是元组?

此外:

try:
  raise (Exception, 'a message')
except Exception as ex:
  print ex

这里我们实际上是抛出了一个异常 (与之前的例子中抛出 int 相比,有一致的行为) - 我曾经简要地认为这只是另一种方式:

try:
  raise Exception, 'a message'
except Exception as ex:
  print ex

但是在这种情况下,'a message'被传递给了Exceptions构造函数(如docs.python.org上所述)

有人能解释第二和第三种情况,并可能指向负责此操作的解释器代码吗?

3个回答

18

根据 Python 2 参考文档raise语句最多可以使用3个表达式来创建被抛出的异常:

raise_stmt ::= "raise" [expression ["," expression ["," expression]]]

如果第一个表达式是一个元组,Python将递归地“解包”该元组,一直取第一个元素,直到找到一个不是元组的元素为止。这种行为在 Python 3 中被移除(参见PEP 3109)。以下代码是合法的:

>>> raise ((Exception, 'ignored'), 'ignored'), 'something', None
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
Exception: something
文档详细解释了剩下的内容,但是raise语句希望第一个值是一个异常类(Exception class),第二个值被视为异常的值(消息),第三个值是一个回溯(traceback)。如果缺少后两个值,Python会用None填充。如果第一个值是一个实例(instance),第二个值必须为None。
>>> raise Exception('something'), 'something', None
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: instance exception may not have a separate value

如果您使用的元组超过3个项目,则会引发语法错误:

>>> raise Exception, 'something', None, None
  File "<stdin>", line 1
    raise Exception, 'something', None, None
                                      ^
SyntaxError: invalid syntax

然而在你的情况下,你既没有定义一个类也没有创建一个实例,因此Python首先发现这个错误;如果我使用一个字符串,它也会抱怨:

>>> raise 'not an exception', 'something', None
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: exceptions must be old-style classes or derived from BaseException, not str

正确的语法当然是:

>>> raise Exception, 'something', None
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
Exception: something

我之前不知道递归元组展开的方法,所以我无法理解为什么其他元素被忽略了 - 这个PEP是我一直在寻找的缺失环节。 - dahpgjgamgan
你链接的文档中没有展示相同的语法。它说 raise_stmt ::= "raise" [expression ["from" expression]] - Barmar
@Barmar:链接已更正。当回答这个问题时,链接指向Python 2版本,现在它重定向到3版本。 - Martijn Pieters

3

http://docs.python.org/reference/simple_stmts.html#the-raise-statement

"raise" [expression ["," expression ["," expression]]]

如果没有表达式出现,raise会重新抛出在当前作用域中处于活动状态的最后一个异常...否则,raise评估表达式以获取三个对象,对于省略的表达式,使用None作为值。前两个对象用于确定异常的类型和值。

实际上,我认为Python在这里执行元组解包

try:
    raise (ValueError, "foo", ), "bar"
except Exception as e:
    print e.message # foo or bar?

但是如果它确实这样做了,结果将是"foo",而不是"bar"。这种行为似乎没有在任何地方记录,只有一个简短的注释说明在py3中被删除了:

在Python 2中,以下raise语句是合法的

raise ((E1, (E2, E3)), E4), V

解释器将以递归方式将元组的第一个元素作为异常类型,使上述内容完全等同于

raise E1, V

从Python 3.0开始,将放弃支持像这样提高元组的能力。此更改将使raise语句与生成器对象上的throw()方法保持一致,后者已禁止这种操作。

http://www.python.org/dev/peps/pep-3109/#id17


1
我不明白那怎么回答了问题。能解释一下吗? - Felix Kling
1
为了让我的评论更清楚一些:显然,元组是被评估的,因此返回第一个元素。为什么使用列表参数时不会发生这种情况呢? - Felix Kling
似乎元组在raise时会自动解包(而列表则不能解包)。 - mutantacule

3

显然,尽管文档中指定了第一个表达式必须为空,Python也接受非空元组作为raise语句的第一个表达式(但正如此PEP所述),如果是元组,则递归使用其第一个元素作为异常类。让我给你展示一些代码:

>>> raise ValueError, 'sdf', None
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: sdf

>>> raise (ValueError, 5), 'sdf', None
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: sdf

虽然我之前的评论有所提及,但实际上并没有自动解压缩功能,因为在我的下一个示例中,字符串并未传递给异常类:

>>> raise (ValueError, 'sdf', None)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError

此外,使用Python的ast模块,我们可以看到在raise表达式中默认情况下没有元组:
>>> ast.dump(ast.parse('raise ValueError, "asd"'))
"Module(body=[Raise(type=Name(id='ValueError', ctx=Load()), inst=Str(s='asd'), tback=None)])"

如果我们使用元组,那么它将作为类型参数传递:

>>> ast.dump(ast.parse('raise (ValueError, "asd")'))
"Module(body=[Raise(type=Tuple(elts=[Name(id='ValueError', ctx=Load()), Str(s='asd')], ctx=Load()), inst=None, tback=None)])"

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接