处理sys.exc_info()返回的回溯对象时的正确注意事项和安全性

9 投票
1 回答
1781 浏览
提问于 2025-04-17 02:00

我知道 sys.exc_info 的文档提到在处理 traceback 对象时要小心,但我还是不太确定某些情况到底安全还是不安全。此外,文档里说“警告:不要这样做!”,紧接着又说“注意:其实可以”,这让我更加困惑。

无论如何,文档和为什么需要显式删除 sys.exc_info() 的 traceback(Alex Martelli 的回答)似乎暗示,只有那些引用了 traceback 值的局部变量才会引发问题。

这让我有几个问题:

  1. 在这个上下文中,“局部变量”到底是什么意思?我对术语有点困惑,但这是否意味着只有在函数内部创建的变量,还是包括函数参数创建的变量?那其他作用域内的变量,比如全局变量或 self 呢?
  2. 闭包如何影响 traceback 的潜在循环引用?一般的想法是:闭包可以引用它所包围的函数能引用的所有东西,所以一个引用了闭包的 traceback 可能会引用很多东西。我在想一个更具体的例子,但可能是某种组合:一个内部函数,返回 sys.exc_info() 的代码,以及某处有短命但开销大的对象。

如果我得出的结论或假设有错,请随时告诉我,因为在写这个的过程中,我已经多次对自己的说法产生了信念和怀疑 :)。

虽然我希望能得到我具体例子的答案,但我也在寻求一些关于如何在更复杂的情况下安全处理 traceback 的一般建议、知识或经验分享(例如,你需要运行一个循环并想要收集任何抛出的异常,你需要启动一个新线程并报告它抛出的异常,你需要创建闭包和回调并需要传递抛出的异常等等)。

例子 1:一个处理错误的内部函数

def DoWebRequest():
  thread, error_queue = CreateThread(ErrorRaisingFunc)
  thread.start()
  thread.join()
  if not error_queue.empty():
    # Purposefully not calling error_queue.get() for illustrative purposes
    print 'error!'

def CreateThread(func):
  error_queue = Queue.Queue()
  def Handled():
    try:
      func()
    except Exception:
      error_queue.put(sys.exc_info())
  thread = threading.Thread(target=Handled)
  return thread, error_queue

这个 Handled() 闭包会导致任何抛出的异常引用 error_queue 吗?这样会导致循环引用,因为 error_queue 也包含 traceback 吗?从 error_queue 中移除 traceback(也就是调用 .get())是否足以消除循环引用?

例子 2:在 exc_info 范围内的长生命周期对象,或者返回 exc_info

long_lived_cache = {}

def Alpha(key):
  expensive_object = long_lived_cache.get(key)
  if not expensive_object:
    expensive_object = ComputeExpensiveObject()
    long_lived_cache[key] = expensive_object

  exc_info = AlphaSub(expensive_object)
  if exc_info:
    print 'error!', exc_info

def AlphaSub(expensive_object):
  try:
    ErrorRaisingFunc(expensive_object)
    return None
  except Exception:
    return sys.exc_info()

抛出的异常 AlphaSub() 是否引用了 expensive_object,而且因为 expensive_object 被缓存,traceback 永远不会消失?如果是这样,怎么打破这样的循环?

另外,exc_info 包含 Alpha 的栈帧,而 Alpha 的栈帧又引用了 exc_info,导致循环引用。如果是这样,怎么打破这样的循环?

1 个回答

4

在这个上下文中,“局部变量”到底是什么意思呢?我对术语有点困惑:这是否意味着只有在函数内部创建的变量,还是包括函数参数创建的变量?那其他范围内的变量,比如全局变量或者self呢?

“局部变量”是指在函数内部创建的所有变量。这包括任何函数参数和任何被赋值的变量。例如,在下面的代码中:

def func(fruwappah, qitzy=None):
    if fruwappah:
        fruit_cake = 'plain'
    else:
        fruit_cake = qitzy
    frosting = 'orange'

变量 fruwappahqitzyfruit_cakefrosting 都是局部变量。哦,还有,因为 self 在函数头里(虽然在我的例子中没有),它也是局部的。

闭包是如何影响追踪回溯中的潜在循环引用的?一般的想法是:闭包可以引用它所包含的函数的所有内容,因此带有闭包引用的追踪回溯可能会引用很多东西。我在想一个更具体的例子,但可能会涉及到:一个内部函数、返回 sys.exc_info() 的代码,以及某个地方的短暂且开销大的对象。

正如你链接的答案所说:追踪回溯会引用在异常发生时所有活跃的函数(及其变量)。换句话说,是否涉及闭包并不重要——给闭包(非局部)或全局变量赋值都会产生循环引用。

处理这个问题有两种基本方法:

  1. 定义一个在异常被抛出后会被调用的函数——它在追踪回溯中不会有栈帧,所以当它结束时,所有变量,包括追踪回溯,都会消失;或者
  2. 确保在完成后使用 del traceback_object 来删除它。

说了这些,我在自己的代码中还没有需要追踪回溯对象的情况——异常及其各种属性到目前为止已经足够了。

撰写回答