我该如何修改上面的测试脚本以避免在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
函数中实现。
- 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
.
- 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
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);
}
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);
}
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除非您了解之前脚注中提到的属性缓存机制,否则这种特殊的行为是唯一没有意义的部分。
generateOutput.py | less
命令并在第一屏上退出less
时,人们应该如何避免这个错误?不写入sys.stdout
(或完全不输出)并不是一个很好的解决办法。这和“你可以通过不用Python编写代码来避免这个错误”一样有用。 - jamesdlinsys.stderr.close(); sys.stdout.close()
。 - badp