捕获异常后try-except内部的更改会持续吗

17 投票
4 回答
4850 浏览
提问于 2025-04-16 10:20



我第一次接触异常处理的时候(不是在Python中),我一直以为当你开始一个try块时,就像是在一个沙盒里写东西:如果发生了异常,try块里面发生的所有事情就好像从来没有发生过。没想到在Python中,这并不是我想的那样。以下是我在Python中的实验:

>>> a = range(5)
>>> a
[0, 1, 2, 3, 4]
>>> try:
...     a.append(5)
...     oops
... except:
...     raise
... 
Traceback (most recent call last):
  File "<stdin>", line 3, in <module>
NameError: name 'oops' is not defined
>>> print a
[0, 1, 2, 3, 4, 5]

从结果可以看到,我在try块里面修改了列表,然后触发了一个错误,这个错误被抛出了。我原本以为列表会恢复到最初的样子,[0, 1, 2, 3, 4],但是a.append(5)的结果却保留了下来。

难道我一开始的期待就是错的吗?也许我的期待部分是错的(可能有沙盒,但它并不是那样运作的)?

4 个回答

2

你的期望有点不切实际。也许这会是个不错的功能(在SQL中有类似的功能叫做事务,但在编程语言中很少见,而且大多数研究都是在一些没人用的语言上进行的),但在任何常用的语言中都无法实现(更别提效率问题了——因为编译器或解释器通常对程序员在做什么知之甚少,你需要保存整个程序的状态并恢复它,但这仍然无法捕捉到解释器外的所有副作用,比如文件输入输出)。

try块的意思就是“尝试去做这个,如果失败了就跳过去做那个”。注意,只有后半部分是特别的——如果在try块外发生了异常,执行会跳到调用图中的下一个try,如果没有的话,就会跳到一个全局处理程序,打印出异常信息并结束程序的执行。

2

你的想法在几乎所有支持异常处理的编程语言中都是错误的——在 try 块中并没有“要么全部成功,要么全部失败”的规则(虽然在某些语言中可能有事务的概念,比如支持事务性内存的语言)。

实际上,只有在异常发生后,try 块中后面的部分不会再执行了。

9

你刚刚发现,异常处理并不是解决错误的万能钥匙。

异常并不能保护你免受状态变化的影响……在异常被抛出之前完成的任何操作都必须被撤销。这就是在Python、C++、Java以及许多其他语言中异常的工作方式。

有时候,你可能会有一种“外部”的一般保护:比如说,如果你所做的只是对一个支持事务的数据库进行更改,那么你可以在最外层捕获异常时执行“回滚”,而不是提交更改,这样你就能得到你想要的保护。如果没有这样的自然“墙”来保护你免受部分状态变化的影响,那么处理起来就会困难得多。

已经完成的操作不会被撤销,这正是使得使用异常处理变得不简单的原因,尤其是当问题的复杂性增加时。

通常,代码可以在几个层面上被分类为异常“安全”:

  1. 如果发生异常,一切都完蛋了,甚至无法干净地退出或重启。这通常被认为是“不安全的异常处理”。

  2. 如果发生异常,代码不会完成它的工作,子系统(类实例、库)的状态是无效的。不过你可以安全地重启(例如,你可以销毁实例或重新初始化库)。这是最低限度的异常安全。

  3. 如果发生异常,代码不会完成它的工作,子系统的状态是有效但未指定的。调用代码可能会尝试检查当前状态并继续使用子系统,而不是重新初始化它。这比第二种情况稍微好一点。

  4. 如果发生异常,代码不会做任何事情,程序状态保持不变。因此,要么请求在没有错误的情况下完成,要么返回错误信号给调用者,且没有任何改变。这当然是最好的行为。

异常处理最大的问题在于,即使你有两个非常安全的类型4的代码块 AB,简单地将它们顺序组合成 AB 也并不安全,因为如果 B 出现问题,你还必须撤销 A 已经完成的部分。此外,如果在执行 1/A 时可能会出现异常(也就是说,当你试图撤销 A 完成的内容时),那你就麻烦大了,因为你既无法执行 B,也无法恢复到之前的状态(这意味着将 AB 实现为类型4的操作几乎是不可能的)。

换句话说,好的构件并不能简单地构建出好的结构(在异常安全方面)。

撰写回答