48

注意:我没有尝试在 Windows 下或使用 2.7.3 以外的 Python 版本重现下面描述的问题。

引发相关问题的最可靠方法是将以下测试脚本的输出通过管道:(在 下bash):

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

IE:

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

我的问题是:

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

(如测试脚本所示,错误不能用try-except.)

诚然,上面的例子是高度人为的,但是当我的脚本的输出通过一些第 3 方软件传输时,我有时会遇到同样的问题。

错误消息当然是无害的,但它让最终用户感到不安,所以我想让它保持沉默。

编辑:以下脚本与上面的原始脚本的不同之处仅在于它重新定义了 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

4 回答 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 解释器的关闭序列,它Py_Finalizepythonrun.c.

  1. 在调用退出钩子并关闭线程后,终结代码调用PyImport_Cleanup以终结和释放所有导入的模块。该函数执行的倒数第二个任务是删除sys模块,主要包括调用_PyModule_Clear以清除模块字典中的所有条目 - 特别包括标准流对象(Python 对象),例如stdoutstderr
  2. 当一个值从字典中删除或被一个新值替换时,它引用计数使用递减。引用计数为零的对象有资格进行释放。由于模块保存了对标准流对象的最后剩余引用,因此当这些引用被取消设置时,它们就可以被释放了。1Py_DECREFsys_PyModule_Clear
  3. Python文件对象file_dealloc释放是. fileobject.c这首先使用恰当命名的函数调用 Python 文件对象的close方法close_the_file

    ret = close_the_file(f);
    

    对于标准文件对象,close_the_file(f) 委托给 Cfclose函数,如果仍有数据要写入文件指针,该函数会设置错误条件。file_dealloc然后检查该错误情况并打印您看到的第一条消息:

    if (!ret) {
        PySys_WriteStderr("close failed in file object destructor:\n");
        PyErr_Print();
    }
    else {
        Py_DECREF(ret);
    }
    
  4. 打印该消息后,Python 会尝试使用PyErr_Print. 它委托给PyErr_PrintEx,并作为其功能的一部分,PyErr_PrintEx尝试从sys.excepthook.

    hook = PySys_GetObject("excepthook");
    

    如果在 Python 程序的正常过程中完成,这会很好,但在这种情况下,sys.excepthook已经被清除了。2 Python 检查此错误情况并打印第二条消息作为通知。

    if (hook && hook != Py_None) {
        ...
    } else {
        PySys_WriteStderr("sys.excepthook is missing\n");
        PyErr_Display(exception, v, tb);
    }
    
  5. 在通知我们缺少 后excepthook,Python 然后回退到使用 打印异常信息PyErr_Display,这是显示堆栈跟踪的默认方法。这个函数做的第一件事就是尝试访问sys.stderr.

    PyObject *f = PySys_GetObject("stderr");
    

    在这种情况下,这不起作用,因为sys.stderr已经被清除并且无法访问。3因此,代码直接调用fprintf将第三条消息发送到 C 标准错误流。

    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除非您了解前面脚注中提到的属性缓存机制,否则此特定行为是唯一没有意义的部分。

于 2012-10-09T19:42:15.010 回答
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

然后这个脚本什么也没有发生,因为异常(IOError:[Errno 32] Broken pipe)被try...except抑制。

$ python testscript.py  | :
$
于 2014-07-25T17:05:40.903 回答
2

在您的程序中抛出一个无法使用 try/except 块捕获的异常。要抓住他,请覆盖函数sys.excepthook

import sys
sys.excepthook = lambda *args: None

文档

sys.excepthook(类型,值,回溯)

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

说明性示例:

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
于 2012-10-08T22:31:16.020 回答
-6

我意识到这是一个老问题,但我在谷歌搜索错误时发现了它。就我而言,这是一个编码错误。我最后的声明之一是:

print "Good Bye"

解决方案只是将语法修复为:

print ("Good Bye")

[树莓派零,Python 2.7.9]

于 2017-03-18T02:29:13.303 回答