如何消除“sys.excepthook is missing”错误?

48

注意:我没有尝试在Windows上,或者除了2.7.3版本之外的Python版本中重现下面描述的问题。

引出这个问题最可靠的方法是将以下测试脚本的输出通过:(在bash下)进行管道传输:

try:
    for n in range(20):
        print n
except:
    pass

即:

% python testscript.py | :
close failed in file object destructor:
sys.excepthook is missing
lost sys.stderr

我的问题是:

如何修改上面的测试脚本,以避免在运行脚本时(在Unix/bash下)出现错误消息?

(正如测试脚本所示,无法使用try-except捕获错误。)

以上示例虽然高度人工制作,但我有时会遇到相同的问题,当我的脚本输出被某些第三方软件管道传输时。

错误信息肯定是无害的,但这让最终用户感到不安,因此我想将其消除。

编辑:以下脚本与上面原始脚本的唯一区别在于它重新定义了sys.excepthook,在行为上与给定的脚本完全相同。

import sys
STDERR = sys.stderr
def excepthook(*args):
    print >> STDERR, 'caught'
    print >> STDERR, args

sys.excepthook = excepthook

try:
    for n in range(20):
        print n
except:
    pass
4个回答

71
我该如何修改上面的测试脚本以避免在Unix/bash下运行时出现错误消息?
您需要防止脚本向标准输出写入任何内容。这意味着删除任何print语句和任何使用sys.stdout.write的代码,以及调用它们的任何代码。
之所以会发生这种情况,是因为您正在将非零数量的输出从Python脚本传输到从未从标准输入读取的内容。这不是:命令所特有的;您可以通过管道传输到任何不读取标准输入的命令来获得相同的结果,例如:
python testscript.py | cd .

如果需要一个更简单的例子,可以考虑一个名为printer.py的脚本,它仅包含以下内容:

print 'abcde'

那么

python printer.py | python printer.py

会产生相同的错误。

当你将一个程序的输出导入另一个程序时,写入程序产生的输出会被缓存在一个缓冲区中,并等待读取程序从缓冲区请求数据。只要缓冲区非空,任何试图关闭写入文件对象的尝试都应该失败并出现错误。这就是你看到的消息的根本原因。

触发错误的具体代码在Python的C语言实现中,这就解释了为什么你无法用try/except块捕获它:它在你的脚本内容处理完成后运行。基本上,在Python关闭自身时,它会尝试关闭stdout,但由于仍有缓冲输出等待读取,因此失败。所以Python试图像通常一样报告这个错误,但sys.excepthook已经在最终化过程中被移除,因此失败。然后Python尝试向sys.stderr打印一条消息,但它已经被释放,因此再次失败。你在屏幕上看到消息的原因是Python代码确实包含一个备选方案fprintf,可以直接将一些输出写入文件指针,即使Python的输出对象不存在。

技术细节

对于那些对此过程的细节感兴趣的人,让我们来看看Python解释器的关闭序列,该序列在pythonrun.cPy_Finalize函数中实现。

  1. After invoking exit hooks and shutting down threads, the finalization code calls PyImport_Cleanup to finalize and deallocate all imported modules. The next-to-last task performed by this function is removing the sys module, which mainly consists of calling _PyModule_Clear to clear all the entries in the module's dictionary - including, in particular, the standard stream objects (the Python objects) such as stdout and stderr.
  2. When a value is removed from a dictionary or replaced by a new value, its reference count is decremented using the Py_DECREF macro. Objects whose reference count reaches zero become eligible for deallocation. Since the sys module holds the last remaining references to the standard stream objects, when those references are unset by _PyModule_Clear, they are then ready to be deallocated.1
  3. Deallocation of a Python file object is accomplished by the file_dealloc function in fileobject.c. This first invokes the Python file object's close method using the aptly-named close_the_file function:

    ret = close_the_file(f);
    

    For a standard file object, close_the_file(f) delegates to the C fclose function, which sets an error condition if there is still data to be written to the file pointer. file_dealloc then checks for that error condition and prints the first message you see:

    if (!ret) {
        PySys_WriteStderr("close failed in file object destructor:\n");
        PyErr_Print();
    }
    else {
        Py_DECREF(ret);
    }
    
  4. After printing that message, Python then attempts to display the exception using PyErr_Print. That delegates to PyErr_PrintEx, and as part of its functionality, PyErr_PrintEx attempts to access the Python exception printer from sys.excepthook.

    hook = PySys_GetObject("excepthook");
    

    This would be fine if done in the normal course of a Python program, but in this situation, sys.excepthook has already been cleared.2 Python checks for this error condition and prints the second message as a notification.

    if (hook && hook != Py_None) {
        ...
    } else {
        PySys_WriteStderr("sys.excepthook is missing\n");
        PyErr_Display(exception, v, tb);
    }
    
  5. After notifying us about the missing excepthook, Python then falls back to printing the exception info using PyErr_Display, which is the default method for displaying a stack trace. The very first thing this function does is try to access sys.stderr.

    PyObject *f = PySys_GetObject("stderr");
    

    In this case, that doesn't work because sys.stderr has already been cleared and is inaccessible.3 So the code invokes fprintf directly to send the third message to the C standard error stream.

    if (f == NULL || f == Py_None)
        fprintf(stderr, "lost sys.stderr\n");
    
有趣的是,在Python 3.4+中,行为略有不同,因为在清除内置模块之前,终止过程现在会显式刷新标准输出和错误流。这样,如果您有等待写入的数据,则会收到明确指示该条件的错误,而不是在正常终止过程中发生“意外”故障。此外,如果您运行
python printer.py | python printer.py

使用Python 3.4(当然,在print语句上加括号),您将不会得到任何错误。我想第二次调用Python可能因某种原因而消耗标准输入,但这是一个完全不同的问题。

1实际上,这是个谎言。Python的导入机制会缓存每个导入模块的字典副本, 直到_PyImport_Fini运行, Py_Finalize的实现中稍后, 那时标准流对象的最后引用才会消失。一旦引用计数达到零,Py_DECREF会立即释放这些对象。
但是对于主要问题来说,重要的是从sys模块的字典中删除引用,然后在稍后的某个时间释放它们。

2这是因为在真正释放任何内容之前,sys 模块的字典被完全清除,这要归功于属性缓存机制。您可以使用 -vv 选项运行 Python,以查看在关闭文件指针的错误消息出现之前取消设置所有模块属性。

3除非您了解之前脚注中提到的属性缓存机制,否则这种特殊的行为是唯一没有意义的部分。


1
一个非常清晰简洁的解释,通常这种情况下我会像淹水一样需要氧气罐(喘不过气来),谢谢! - matt wilkie
13
当使用generateOutput.py | less命令并在第一屏上退出less时,人们应该如何避免这个错误?不写入sys.stdout(或完全不输出)并不是一个很好的解决办法。这和“你可以通过不用Python编写代码来避免这个错误”一样有用。 - jamesdlin
3
确实,这是应该在Python解释器本身中修复的问题:http://bugs.python.org/issue11380 - jamesdlin
2
@DavidZ 最明显的情况是当输出被管道传输到 head 命令时,这是一个非常常见的用例,如果你想在重定向到文件之前检查输出。 - Ian Sudbery
1
另一种可能不好的消除错误的方法是:在脚本的最后加上 sys.stderr.close(); sys.stdout.close() - badp
显示剩余5条评论

11

今天我自己遇到了这种问题并寻找答案。我认为一个简单的解决方法是确保您先刷新stdio,这样Python在脚本关闭期间不会失败而是阻塞。例如:

--- a/testscript.py
+++ b/testscript.py
@@ -9,5 +9,6 @@ sys.excepthook = excepthook
 try:
     for n in range(20):
         print n
+    sys.stdout.flush()
 except:
     pass

使用这个脚本时,由于try...except语句捕获了异常(IOError: [Errno 32] Broken pipe),所以什么都不会发生。

$ python testscript.py  | :
$

2

如果您的程序抛出一个无法使用try/except块捕获的异常,那么可以通过重写sys.excepthook函数来捕获它:

import sys
sys.excepthook = lambda *args: None

来自文档

sys.excepthook(type, value, traceback)

当引发并未捕获的异常时,解释器会使用三个参数调用sys.excepthook函数,分别是异常类、异常实例和一个回溯(traceback)对象。在交互式会话中,这仅在控制权返回到提示符之前发生;在Python程序中,这仅在程序退出之前发生。可以通过分配另一个带有三个参数的函数来自定义处理此类顶层异常的方法。

举例说明:

import sys
import logging

def log_uncaught_exceptions(exception_type, exception, tb):

    logging.critical(''.join(traceback.format_tb(tb)))
    logging.critical('{0}: {1}'.format(exception_type, exception))

sys.excepthook = log_uncaught_exceptions

1
尝试过使用我提供的测试脚本来验证这个解决方案吗?毕竟,这也是我提供它的原因... - kjo

-6

我知道这是一个老问题,但我在谷歌搜索错误时找到了它。在我的情况下,这是一个编码错误。我的最后一条语句之一是:

print "Good Bye"

解决方案就是简单地修复语法为:

print ("Good Bye")

[树莓派Zero,Python 2.7.9]


1
在Python 2.7.9中,使用print时加上括号和不加括号没有区别。 - DYZ

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