如何修改上面的测试脚本以避免在脚本如图所示运行时出现错误消息(在 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_Finalize
是在pythonrun.c
.
- 在调用退出钩子并关闭线程后,终结代码调用
PyImport_Cleanup
以终结和释放所有导入的模块。该函数执行的倒数第二个任务是删除sys
模块,主要包括调用_PyModule_Clear
以清除模块字典中的所有条目 - 特别包括标准流对象(Python 对象),例如stdout
和stderr
。
- 当一个值从字典中删除或被一个新值替换时,它的引用计数使用宏递减。引用计数为零的对象有资格进行释放。由于模块保存了对标准流对象的最后剩余引用,因此当这些引用被取消设置时,它们就可以被释放了。1
Py_DECREF
sys
_PyModule_Clear
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);
}
打印该消息后,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);
}
在通知我们缺少 后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除非您了解前面脚注中提到的属性缓存机制,否则此特定行为是唯一没有意义的部分。