【问题标题】:How to prevent BrokenPipeError when doing a flush in Python?在 Python 中进行刷新时如何防止 BrokenPipeError?
【发布时间】:2014-12-28 19:30:56
【问题描述】:

问题:有没有办法在不获取BrokenPipeError 的情况下将flush=True 用于print() 函数?

我有一个脚本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()

【问题讨论】:

  • 我无法在 OS X 10.10 上重现它,现在尝试 centOS 6.6。
  • 我刚刚在 OS X 10.9.4 上尝试过,但无法重现它。我在 Ubuntu 12.04.2 LTS 上遇到错误。我会在 Linux Mint Qiana 上尝试。
  • 你所有的脚本都为我打断了,除了第一个......
  • 我无法通过在 Python 3.4.1 中运行您的脚本来重现异常。您使用的是哪个 Python 版本?
  • 我在 Mac OS X 10.9.4 上尝试了 3.4.1 版本,在 Ubuntu 12.04.2 LTS 上尝试了 3.3.2 版本。我目前无法自己重现该错误。我不得不将 range(4) 更改为 range(4000) 并将 head -n3 更改为 head -n3000 以重现错误。

标签: python unix python-3.x flush broken-pipe


【解决方案1】:

回答

import sys
for i in range(4000):
    try:
        print(i, flush=True)
    except BrokenPipeError:
        sys.stdout = None

说明

即使您捕获了 BrokenPipeError 异常,当您的程序退出并且 Python 尝试刷新标准输出时,Python 也会再次抛出该异常。通过将 stdout 设置为 None,Python 将不会尝试刷新它。

缺点

虽然 Python 例程(例如 print())正确检查 stdout 是否为 None 并且不会失败,但不检查的程序并不罕见。如果您的程序在将 stdout 设置为 None 之后尝试使用 stdout.write() 或类似的,那么 Python 将抛出一个 AttributeError。

其他答案(以及为什么不)

没有答案比sys.stdout = None 更短或更简单,但一些常见的答案存在重大问题。

/dev/null

Python 开发人员 have their own suggested code 处理 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,而不仅仅是标准输出,所以如果你的整个程序会突然神秘地死掉,例如,它正在写入到连接被中断的网络套接字。

pipe.py | something | head

一个非 Python 解决方案是首先将 stdout 传送到一个程序,该程序将继续从 Python 程序读取数据,即使它自己的标准输出已关闭。例如,假设您拥有tee 的 GNU 版本,则可以:

pipe.py | tee -p /dev/null | head

为什么不:问题是在 Python 中寻找答案。此外,它会使 pipe.py 运行的时间超过其所需的时间,这可能会消耗大量资源,这也是次优的。

【讨论】:

    【解决方案2】:

    在 Python 3.7 文档中,note on SIGPIPEadded,建议以这种方式捕获 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()
    

    重要的是,它说:

    不要将SIGPIPE的处置设置为SIG_DFL以避免BrokenPipeError。这样做会导致您的程序在您的程序仍在写入时任何套接字连接被中断时意外退出。

    【讨论】:

    • 这似乎是最新的答案,也是官方文档中推荐的方法。在 Mac OS 10.14 / Python 3.7.5 上测试
    • 我想知道将flush=True 传递给print 是否与sys.stdout.flush() 行具有相同的效果。
    • 不想退出怎么办?
    • 别介意我之前的评论。我已经在下面发布了我自己的答案。
    【解决方案3】:

    我经常希望有一个命令行选项来抑制这些信号处理程序。

    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 开发人员明确表示不应该这样做。 docs.python.org/3/library/signal.html#note-on-sigpipe
    • 如果您的程序在退出前需要做一些清理工作,那是正确的——您不希望将 SIGINT 或 SIGPIPE 设置为 SIG_DFL。此外,在这种情况下,您还想捕捉很多很多信号。
    • 我对其进行了更多研究,似乎 SIGPIPE 没有指定哪个文件描述符具有损坏的管道,因此将其设置为 SIG_DFL 可能会不恰当地终止程序。例如,如果您的 Python 程序正在访问网络并且套接字连接已重置,则您可以获得 SIGPIPE。
    • 哦,我发现有一个更简单的解决方案。问题原来是 Python 本身在退出时刷新标准输出,导致第二个 BrokenPipeError。只需在异常处理程序中设置stdout = None,Python 不会这样做。
    【解决方案4】:

    虽然其他人已经详细介绍了潜在问题,但有一个简单的解决方法:

    python whatever.py | tail -n +1 | head -n3000
    

    解释:tail 缓冲直到它的 STDIN 被关闭(python 退出并关闭它的 STDOUT)。所以只有 tail 在 head 退出时获得 SIGPIPE。 -n +1 实际上是一个空操作,使尾部输出从第 1 行开始的“尾部”,即整个缓冲区。

    【讨论】:

    • 您是否使用较小的 head 值对此进行了测试?它对我不起作用。
    【解决方案5】:

    暂时忽略 SIGPPIE

    我不确定这是多么糟糕的想法,但它确实有效:

    #!/usr/bin/env python3
    
    import signal
    import sys
    
    sigpipe_old = signal.getsignal(signal.SIGPIPE)
    signal.signal(signal.SIGPIPE, signal.SIG_DFL)
    for i in range(4000):
        print(i, flush=True)
    signal.signal(signal.SIGPIPE, sigpipe_old)
    

    【讨论】:

    【解决方案6】:

    正如您在发布的输出中看到的那样,在析构函数阶段引发了最后一个异常:这就是为什么最后有 ignored

    Exception BrokenPipeError: BrokenPipeError(32, 'Broken pipe') in <_io.TextIOWrapper name='<stdout>' mode='w' encoding='UTF-8'> ignored
    

    一个简单的例子来理解在这种情况下发生了什么:

    >> class A():
    ...     def __del__(self):
    ...         raise Exception("It will be ignored!!!")
    ... 
    >>> a = A()
    >>> del a
    Exception Exception: Exception('It will be ignored!!!',) in <bound method A.__del__ of <__builtin__.A instance at 0x7ff1d5c06d88>> ignored
    >>> a = A()
    >>> import sys
    >>> sys.stderr.close()
    >>> del a
    

    在对象被销毁时触发的每个异常都会导致标准错误输出,解释异常发生和被忽略(这是因为 python 会通知您在销毁阶段无法正确处理某些事情)。无论如何,这种异常无法缓存,因此您可以删除可以生成它的调用或关闭stderr

    回到问题上来。该异常不是真正的问题(因为它被忽略了)但是如果您不想打印它,则必须覆盖在对象将被销毁或关闭时可以调用的函数@ 987654325@正如@SergeBallesta正确建议的那样:在您的情况下,您可以 shutdown writeflush 函数,并且在销毁上下文中不会触发异常

    这是你如何做到这一点的一个例子:

    import sys
    def _void_f(*args,**kwargs):
        pass
    
    for i in range(4000):
        try:
            print(i,flush=True)
        except (BrokenPipeError, IOError):
            sys.stdout.write = _void_f
            sys.stdout.flush = _void_f
            sys.exit()
    

    【讨论】:

    • 这不是解决方案。他希望能够将它与flush=Trueprint 一起使用。
    • 在我写的时候,我在 python 3.2 上测试了它......现在我将它适配到 python 3.4
    • @phantom 那是printflush=True 的版本...这不是一个解决方案吗?
    • 这也有效。非常感谢!我从@serge-ballesta 中选择了答案,因为他的回复速度稍快一些,并且避免了新函数的定义。谢谢你们俩。关于异常,我有很多东西要学,我从来没有坐下来读过。
    • @tommy.carstensen 无论如何,我理解您的选择,因为 Sarge Balestra 的答案写得很好,并且提供了一种简单的解决方案。
    【解决方案7】:

    BrokenPipeError 是正常的,因为读取进程(head)终止并关闭了它的管道末端,而写入进程(python)仍在尝试写入。

    is 是异常情况,python 脚本接收到BrokenPipeError - 更准确地说,Python 解释器接收到它捕获的系统 SIGPIPE 信号并引发 BrokenPipeError 以允许脚本处理错误。

    并且您可以有效地处理错误,因为在上一个示例中,您只看到一条消息说异常被忽略 - 好吧这不是真的,但似乎与 Python 中的 open issue 相关:Python 开发人员认为重要警告用户异常情况。

    真正发生的是 AFAIK,python 解释器总是在 stderr 上发出信号,即使你捕获了异常。但是您只需要在退出之前关闭 stderr 即可摆脱该消息。

    我将您的脚本稍微更改为:

    • 像在上一个示例中那样捕获错误
    • 捕获 IOError(我在 Windows64 上的 Python34 中得到)或 BrokenPipeError(在 FreeBSD 9.0 上的 Python 33 中) - 并为此显示一条消息
    • 在 stderr 上显示自定义 Done 消息(stdout 由于管道损坏而关闭)
    • 关闭 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()
    

    【讨论】:

    • 非常酷:) 谢谢!也不知道您可以将异常放入元组中。感谢您也向我展示了这一点。这适用于 Mac OS X 10.9.4 和 Ubuntu 12.04.2 LTS(所有 4 种组合)上的 3.3.2 和 3.4.0。它似乎可以单独使用 BrokenPipeError 和 IOError ; docs.python.org/3/library/… 异常层次结构中的 OSError 的两个部分。
    • 请注意,即使您捕获了异常,Python 3 也会添加一条消息!!! Exception ignored in: &lt;_io.TextIOWrapper name='&lt;stdout&gt;' mode='w' encoding='UTF-8'&gt; stackoverflow.com/questions/16314321/… 在 Python 3.6.8 中测试。
    【解决方案8】:

    根据 Python 文档,这是在以下情况下抛出的:

    在另一端已关闭的情况下尝试在管道上写入

    这是因为 head 实用程序从 stdout 读取,然后立即关闭它

    如您所见,只需在每个print() 之后添加sys.stdout.flush() 即可解决此问题。请注意,这有时在 Python 3 中不起作用。

    您也可以像这样通过管道将其传递给awk 以获得与head -3 相同的结果:

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

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

    【讨论】:

    • 谢谢。不幸的是,awk 解决方法不是一个选项。我不知道实际输出产生多少行。如果你不介意的话,我会在几天内不回答这个问题。再次感谢。
    • @tommy.carstensen sys.stdout.flush() 不起作用吗?另外,对于head,您是否还必须知道它有多少行?
    • @tommy.carstensen 按照您的要求做是不可能的。没有解决方法可以让head 以这种方式与您的程序一起工作。
    • @tommy.carstensen 不会,因为它会根据您将输出输入的程序而有所不同。发生此错误的原因是,当您将输出输入管道的程序在您的 python 脚本之前关闭时,它会关闭输出管道,例如标准输出。您最好使用subprocess 手动管道或使用python 的正则表达式功能而不是grep
    • @tommy.carstensen 我正在关注它。我告诉你这是不可能的。我引用The reason this error occurs is because when the program you pipe output into closes before your python script, it closes the output pipe, e.g. stdout.
    猜你喜欢
    • 2014-05-01
    • 1970-01-01
    • 2021-08-18
    • 1970-01-01
    • 1970-01-01
    • 2012-05-30
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多