为什么在`except`块后使用命名异常会出现`NameError`(或`UnboundLocalError`)?

20 投票
1 回答
1139 浏览
提问于 2025-04-18 10:07

这个示例代码在2.x版本中可以正常运行:

exc = None
try:
    raise Exception
except Exception as exc:
    pass
print(exc)

但是在3.x版本中,我遇到了一个错误,提示 NameError: name 'exc' is not defined。如果我把代码放在一个函数里,我又会收到 UnboundLocalError: local variable 'exc' referenced before assignment 的错误。

我想也许可以通过明确赋值来解决这个问题,像这样:

exc = None
try:
    raise Exception
except Exception as exc:
    exc = exc

但这样也不行。为什么呢?我该如何在 except 块外访问这个异常呢?

1 个回答

26

try语句的作用是明确限制异常的作用范围,这样可以防止出现循环引用的问题,避免异常信息泄露。

当一个异常被赋值给一个目标时,它会在except语句结束时被清除

[...]

这意味着,如果想在except语句之后继续使用这个异常,必须给它赋一个不同的名字。异常会被清除是因为它们带有回溯信息,这样会和调用栈形成一个引用循环,导致栈帧中的所有局部变量在下一次垃圾回收之前都不会被清除。

我强调一下;如果想在后面使用这个异常,必须给它绑定一个新的名字。像exc = exc这样的写法是没用的,因为except并没有创建一个新的作用域,而是将except语句中指定的名字从作用域中移除了。

在Python 2中,异常没有回溯信息的引用,所以不需要清除来防止垃圾回收的问题。而在现在的版本中,异常有了回溯信息,所以这个规则就变了。

不过,即使在Python 2中,你也会被明确警告要清理回溯信息:

警告:在处理异常的函数中,将回溯的返回值赋给一个局部变量会导致循环引用。这会阻止同一函数中局部变量或回溯引用的任何东西被垃圾回收。由于大多数函数不需要访问回溯,最好的解决办法是使用类似exctype, value = sys.exc_info()[:2]的方式,只提取异常类型和异常值。如果确实需要回溯,确保在使用后将其删除(最好用try ... finally语句来完成),或者在一个不处理异常的函数中调用exc_info()

如果你确实重新绑定了异常,可能还想显式地清除回溯信息:

try:
    raise Exception("foo")
except Exception as e:
    exc = e
    exc.__traceback__ = None

撰写回答