如何修改上面的测试脚本以避免如图所示运行脚本时出现错误信息(在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.c的Py_Finalize function中实现。
- 在调用退出钩子并关闭线程后,终结代码调用
PyImport_Cleanup 来终结和释放所有导入的模块。该函数执行的倒数第二个任务是removing the sys module,主要包括调用_PyModule_Clear来清除模块字典中的所有条目——尤其包括标准流对象(Python对象),例如stdout 和 stderr。
- 当一个值从字典中删除或替换为一个新值时,its reference count is decremented 使用the
Py_DECREF macro。引用计数为零的对象有资格进行释放。由于sys 模块保存了对标准流对象的最后剩余引用,因此当_PyModule_Clear 取消设置这些引用时,它们就可以被释放了。1
-
Python 文件对象的释放由the file_dealloc function 中的fileobject.c 完成。这第一个invokes the Python file object's close method 使用了恰当命名的close_the_file function:
ret = close_the_file(f);
对于标准文件对象,close_the_file(f)delegates to the C fclose function,如果仍有数据要写入文件指针,则会设置错误条件。 file_dealloc 然后检查该错误情况并打印您看到的第一条消息:
if (!ret) {
PySys_WriteStderr("close failed in file object destructor:\n");
PyErr_Print();
}
else {
Py_DECREF(ret);
}
-
打印该消息后,Python 会尝试使用PyErr_Print 显示异常。它委托给PyErr_PrintEx,作为其功能的一部分,PyErr_PrintEx 尝试从sys.excepthook 访问 Python 异常打印机。
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);
}
-
在通知我们丢失了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+ 中的行为略有不同,因为在清除内置模块之前,终结过程现在 explicitly flushes the standard output and error streams。这样,如果您有等待写入的数据,您会收到一个明确表示该条件的错误,而不是正常完成过程中的“意外”失败。另外,如果你运行
python printer.py | python printer.py
使用 Python 3.4(当然是在 print 语句上加上括号之后),您根本不会收到任何错误。我想 Python 的第二次调用可能出于某种原因消耗了标准输入,但这是一个完全不同的问题。
1其实那是个谎言。 Python 的导入机制caches a copy of each imported module's dictionary,直到_PyImport_Fini 运行,later in the implementation of Py_Finalize 才发布,那是当对标准流对象的最后引用消失时。一旦引用计数达到零,Py_DECREF立即释放对象。但对于主要答案而言,重要的是引用已从 sys 模块的字典中删除,然后在稍后的某个时间释放。
2同样,这是因为 sys 模块的字典在真正释放任何内容之前已被完全清除,这要归功于属性缓存机制。您可以使用 -vv 选项运行 Python,以在收到有关关闭文件指针的错误消息之前查看所有模块的属性都未设置。
3除非您了解前面脚注中提到的属性缓存机制,否则此特定行为是唯一没有意义的部分。