如何防止在Python中进行flush时出现BrokenPipeError?
问题:有没有办法在使用 print()
函数时加上 flush=True
,而不出现 BrokenPipeError
的错误?
我有一个脚本 pipe.py
:
for i in range(4000):
print(i)
我在Unix命令行中这样调用它:
python3 pipe.py | head -n3000
然后它返回:
0
1
2
这个脚本也是这样:
import sys
for i in range(4000):
print(i)
sys.stdout.flush()
但是,当我运行这个脚本并把它的输出传给 head -n3000
时:
for i in range(4000):
print(i, flush=True)
我就会遇到这个错误:
print(i, flush=True)
BrokenPipeError: [Errno 32] Broken pipe
Exception BrokenPipeError: BrokenPipeError(32, 'Broken pipe') in <_io.TextIOWrapper name='<stdout>' mode='w' encoding='UTF-8'> ignored
我也试过下面的解决方案,但还是出现了 BrokenPipeError
的错误:
import sys
for i in range(4000):
try:
print(i, flush=True)
except BrokenPipeError:
sys.exit()
8 个回答
我常常希望有一个命令行选项可以关闭这些信号处理程序。
import signal
# Don't turn these signal into exceptions, just die.
signal.signal(signal.SIGINT, signal.SIG_DFL)
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
不过,我们能做的最好的办法就是在Python脚本开始运行后尽快卸载这些处理程序。
根据Python的说明,当发生以下情况时,会出现这个错误:
在管道的一端关闭时,尝试在另一端写入数据
这是因为head这个工具会从stdout
读取数据,然后立刻关闭它。
你可以通过在每个print()
后面加一个sys.stdout.flush()
来解决这个问题。需要注意的是,这在Python 3中有时可能不管用。
另外,你也可以像这样把数据传给awk
,这样就能得到和head -3
一样的结果:
python3 0to3.py | awk 'NR >= 4 {exit} 1'
希望这对你有帮助,祝你好运!
回答
import sys
for i in range(4000):
try:
print(i, flush=True)
except BrokenPipeError:
sys.stdout = None
解释
即使你捕获了 BrokenPipeError 这个错误,当你的程序结束时,Python 仍然会在尝试刷新标准输出(stdout)时再次抛出这个错误。通过将 stdout 设置为 None,Python 就不会尝试去刷新它了。
缺点
虽然 Python 的一些功能,比如 print()
,会正确检查 stdout 是否为 None,并且不会出错,但有些程序并不会进行这样的检查。如果你的程序在将 stdout 设置为 None 后,尝试使用 stdout.write()
或类似的功能,Python 就会抛出一个 AttributeError 错误。
其他答案(以及为什么不)
没有哪个答案比 sys.stdout = None
更简短或简单,但一些常见的答案存在重大问题。
/dev/null
Python 开发者 提供了他们自己的处理 BrokenPipeError 的代码。
import os
import sys
def main():
try:
# simulate large output (your code replaces this loop)
for x in range(10000):
print("y")
# flush output here to force SIGPIPE to be triggered
# while inside this try block.
sys.stdout.flush()
except BrokenPipeError:
# Python flushes standard streams on exit; redirect remaining output
# to devnull to avoid another BrokenPipeError at shutdown
devnull = os.open(os.devnull, os.O_WRONLY)
os.dup2(devnull, sys.stdout.fileno())
sys.exit(1) # Python exits with error code 1 on EPIPE
if __name__ == '__main__':
main()
虽然这是标准答案,但它有点奇怪,因为它不必要地打开一个新的文件描述符指向 /dev/null,只是为了让 Python 在关闭之前能刷新它。
为什么不: 对大多数人来说,这毫无意义。这个问题是因为 Python 刷新了一个我们已经捕获了 BrokenPipeError 的句柄。我们知道它会失败,所以解决方案应该是让 Python 根本不去刷新这个句柄。为了让 Python 高兴而分配一个新的文件描述符,实在是太傻了。
为什么(也许): 将 stdout 重定向到 /dev/null 对于一些在收到 BrokenPipeError 后仍然会继续操作 stdout 的程序来说,可能是正确的解决方案。然而,这并不是常见的情况。
sys.stderr.close()
有些人建议关闭 stderr 来隐藏虚假的 BrokenPipe 错误信息。
为什么不: 这样做也会阻止任何合法的错误信息显示出来。
signal(SIGPIPE, SIG_DFL)
另一个常见的答案是使用 SIG_DFL
,默认信号处理器,让程序在接收到 SIGPIPE 信号时直接崩溃。
为什么不: SIGPIPE 可以针对 任何 文件描述符发送,而不仅仅是 stdout,所以如果你的程序正在写入一个网络套接字,而这个连接被中断了,那么整个程序就会突然神秘地崩溃。
pipe.py | something | head
一个非 Python 的解决方案是先将 stdout 管道到一个程序,这个程序即使在 Python 程序的标准输出关闭后也会继续读取数据。例如,假设你有 GNU 版本的 tee
,这样做是可行的:
pipe.py | tee -p /dev/null | head
为什么不: 这个问题是在寻找 Python 中的答案。此外,这种方法并不理想,因为它会让 pipe.py 运行得比必要的时间更长,可能会消耗大量资源。
在Python 3.7的文档中,新增了一条关于SIGPIPE
的说明,并建议用这种方式来捕获BrokenPipeError
:
import os
import sys
def main():
try:
# simulate large output (your code replaces this loop)
for x in range(10000):
print("y")
# flush output here to force SIGPIPE to be triggered
# while inside this try block.
sys.stdout.flush()
except BrokenPipeError:
# Python flushes standard streams on exit; redirect remaining output
# to devnull to avoid another BrokenPipeError at shutdown
devnull = os.open(os.devnull, os.O_WRONLY)
os.dup2(devnull, sys.stdout.fileno())
sys.exit(1) # Python exits with error code 1 on EPIPE
if __name__ == '__main__':
main()
重要的是,它提到:
不要把
的处理方式设置为 SIG_DFL
,以避免出现BrokenPipeError
。这样做会导致你的程序在任何套接字连接中断时意外退出,尤其是在你的程序还在向这个连接写数据的时候。
BrokenPipeError
这个错误是正常的,就像幽灵说的那样,因为读取的过程(head)结束了,关闭了它的管道一端,而写入的过程(python)仍然在尝试写入。
而is则是一个异常情况,python脚本会收到一个BrokenPipeError
- 更准确地说,Python解释器接收到一个系统的SIGPIPE信号,它会捕捉到这个信号并抛出BrokenPipeError
,让脚本能够处理这个错误。
你确实可以处理这个错误,因为在你最后的例子中,你只看到一条消息说这个异常被忽略了 - 其实这不是真的,但似乎和这个Python的开放问题有关:Python的开发者认为有必要提醒用户这个异常情况。
实际上,根据我所知,Python解释器总是在标准错误输出(stderr)上发出这个信号,即使你捕捉到了这个异常。不过,你只需要在退出之前关闭stderr,就可以摆脱这个消息。
我稍微修改了你的脚本,做了以下几点:
- 像你在最后的例子中那样捕捉错误
- 捕捉IOError(我在Windows64的Python34中遇到的)或BrokenPipeError(在FreeBSD 9.0的Python 33中) - 并显示一条消息
- 在标准错误输出上显示一个自定义的完成消息(因为管道断开,标准输出已关闭)
- 在退出之前关闭stderr以去掉消息
这是我使用的脚本:
import sys
try:
for i in range(4000):
print(i, flush=True)
except (BrokenPipeError, IOError):
print ('BrokenPipeError caught', file = sys.stderr)
print ('Done', file=sys.stderr)
sys.stderr.close()
然后这是python3.3 pipe.py | head -10
的结果:
0
1
2
3
4
5
6
7
8
9
BrokenPipeError caught
Done
如果你不想看到多余的消息,只需使用:
import sys
try:
for i in range(4000):
print(i, flush=True)
except (BrokenPipeError, IOError):
pass
sys.stderr.close()