如何防止在Python中进行flush时出现BrokenPipeError?

58 投票
8 回答
70737 浏览
提问于 2025-05-01 01:31

问题:有没有办法在使用 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 个回答

4

我常常希望有一个命令行选项可以关闭这些信号处理程序。

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脚本开始运行后尽快卸载这些处理程序。

4

根据Python的说明,当发生以下情况时,会出现这个错误:

在管道的一端关闭时,尝试在另一端写入数据

这是因为head这个工具会从stdout读取数据,然后立刻关闭它。

你可以通过在每个print()后面加一个sys.stdout.flush()来解决这个问题。需要注意的是,这在Python 3中有时可能不管用。

另外,你也可以像这样把数据传给awk,这样就能得到和head -3一样的结果:

python3 0to3.py | awk 'NR >= 4 {exit} 1'

希望这对你有帮助,祝你好运!

6

回答

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 运行得比必要的时间更长,可能会消耗大量资源。

41

在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。这样做会导致你的程序在任何套接字连接中断时意外退出,尤其是在你的程序还在向这个连接写数据的时候。

65

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()

撰写回答