Python函数如何抑制标准输出和错误输出打印?

61

我有一个使用一些闭源Python函数的Python脚本(即我无法编辑这些函数),这些函数由我的雇主提供。当我调用这些函数时,它们会将输出打印到我的Linux终端上,而我希望抑制输出。我尝试通过以下方式重定向标准输出 / 标准错误流:

orig_out = sys.stdout
sys.stdout = StringIO()
rogue_function()
sys.stdout = orig_out

但是这种方式无法捕获输出。我认为通过Python调用的函数(如上面的rogue_function())实际上是编译的C代码的包装器,它们实际上正在进行打印。

有人知道我如何对通过函数向stdout/stderr传递的任何打印信息进行“深度捕获”(以及该函数调用的任何子函数)吗?

更新:

最终我采取了下面所选答案中概述的方法,并编写了一个上下文管理器来抑制stdout和stderr:

# Define a context manager to suppress stdout and stderr.
class suppress_stdout_stderr(object):
    '''
    A context manager for doing a "deep suppression" of stdout and stderr in 
    Python, i.e. will suppress all print, even if the print originates in a 
    compiled C/Fortran sub-function.
       This will not suppress raised exceptions, since exceptions are printed
    to stderr just before a script exits, and after the context manager has
    exited (at least, I think that is why it lets exceptions through).      

    '''
    def __init__(self):
        # Open a pair of null files
        self.null_fds =  [os.open(os.devnull,os.O_RDWR) for x in range(2)]
        # Save the actual stdout (1) and stderr (2) file descriptors.
        self.save_fds = [os.dup(1), os.dup(2)]

    def __enter__(self):
        # Assign the null pointers to stdout and stderr.
        os.dup2(self.null_fds[0],1)
        os.dup2(self.null_fds[1],2)

    def __exit__(self, *_):
        # Re-assign the real stdout/stderr back to (1) and (2)
        os.dup2(self.save_fds[0],1)
        os.dup2(self.save_fds[1],2)
        # Close all file descriptors
        for fd in self.null_fds + self.save_fds:
            os.close(fd)

使用此功能,只需执行以下操作:

with suppress_stdout_stderr():
    rogue_function()

这个方法“相当不错”。它抑制了那些混乱我的脚本的无用函数的输出。在测试中,我发现它也允许引发异常和一些记录器日志的打印通过,我不是完全清楚其中的原因。我认为这可能与这些消息何时发送到标准输出/标准错误流有关(我认为这是在上下文管理器退出之后发生的)。如果有人可以确认这一点,我会很感兴趣听到详情...


1
这种方法(来自相关侧边栏的链接)有效吗? - Danica
你试过将 sys.stdout 设置为文件而不是 StringIO() 吗?例如:sys.stdout = open('log.txt','w') - carmenism
@Dougal,谢谢,那个有效!如果你愿意的话,请将链接发布为答案,我会选择它。 - jeremiahbuddha
1
请注意,我稍微编辑了这个片段,以便在__exit__中关闭所有文件描述符。如果不关闭self.save_fds中的fd,则每次调用此上下文管理器都会泄漏两个文件描述符,导致长时间运行的进程中的文件描述符耗尽。 - randlet
@randlet - 啊,抱歉,我漏掉了那个。 - shaneb
显示剩余5条评论
9个回答

36

从Python 3.5开始,我们可以利用内置的contextlib模块中的工具进行最小化的操作,主要是使用redirect_stdoutredirect_stderr。我们只需要将这两个内置上下文管理器组合在我们自己的自定义上下文管理器中即可,而这可以通过在Martijn在这里的回答中提供的良好模式中轻松实现。将两个输出重定向到os.devnull应该足够安全和可移植。

from contextlib import contextmanager,redirect_stderr,redirect_stdout
from os import devnull

@contextmanager
def suppress_stdout_stderr():
    """A context manager that redirects stdout and stderr to devnull"""
    with open(devnull, 'w') as fnull:
        with redirect_stderr(fnull) as err, redirect_stdout(fnull) as out:
            yield (err, out)
请注意,抑制stderr仍会在出现错误时提供完整的回溯信息,这是一件好事情:

注:即使抑制了stderr,当出现问题时仍将提供完整的回溯信息,这是一个好现象:

import sys

def rogue_function():
    print('spam to stdout')
    print('important warning', file=sys.stderr)
    1 + 'a'
    return 42

with suppress_stdout_stderr():
    rogue_function()

运行以上代码仅会打印出

Traceback (most recent call last):
  File "tmp.py", line 20, in <module>
    rogue_function()
  File "foo.py", line 16, in rogue_function
    1 + 'a'
TypeError: unsupported operand type(s) for +: 'int' and 'str'

输出到终端。未处理的异常不应该被忽略。


1
这对我来说是最容易实现的事情,谢谢! - Ethan

13

这个方法(通过相关侧边栏找到)可能有效。它重新分配文件描述符,而不仅仅是在sys.stdout等中更改包装器。


7

Python 3.6 已经测试过能够正常工作,经过了数百万次压制测试并没有出现任何错误。

import os
import sys

class suppress_stdout_stderr(object):
    def __enter__(self):
        self.outnull_file = open(os.devnull, 'w')
        self.errnull_file = open(os.devnull, 'w')

        self.old_stdout_fileno_undup    = sys.stdout.fileno()
        self.old_stderr_fileno_undup    = sys.stderr.fileno()

        self.old_stdout_fileno = os.dup ( sys.stdout.fileno() )
        self.old_stderr_fileno = os.dup ( sys.stderr.fileno() )

        self.old_stdout = sys.stdout
        self.old_stderr = sys.stderr

        os.dup2 ( self.outnull_file.fileno(), self.old_stdout_fileno_undup )
        os.dup2 ( self.errnull_file.fileno(), self.old_stderr_fileno_undup )

        sys.stdout = self.outnull_file        
        sys.stderr = self.errnull_file
        return self

    def __exit__(self, *_):        
        sys.stdout = self.old_stdout
        sys.stderr = self.old_stderr

        os.dup2 ( self.old_stdout_fileno, self.old_stdout_fileno_undup )
        os.dup2 ( self.old_stderr_fileno, self.old_stderr_fileno_undup )

        os.close ( self.old_stdout_fileno )
        os.close ( self.old_stderr_fileno )

        self.outnull_file.close()
        self.errnull_file.close()

1
我发现这个比得票最高的答案更可靠。得票最高的答案没有抑制来自底层C++和C库的stdout和stderr。而这个可以做到。 - Duane

3

可能是C代码直接写入了标准输出流stdout,您很可能无法重定向它。抱歉。 - Bob
这些与Python 3兼容吗? - hola

2

虽然不是由提问者请求的,但我需要隐藏并存储输出结果,我按以下方式操作:

from io import StringIO
import sys

class Hider:
    def __init__(self, channels=('stdout',)):
        self._stomach = StringIO()
        self._orig = {ch : None for ch in channels}

    def __enter__(self):
        for ch in self._orig:
            self._orig[ch] = getattr(sys, ch)
            setattr(sys, ch, self)
        return self

    def write(self, string):
        self._stomach.write(string)

    def flush(self):
        pass

    def autopsy(self):
        return self._stomach.getvalue()

    def __exit__(self, *args):
        for ch in self._orig:
            setattr(sys, ch, self._orig[ch])

使用方法:

with Hider() as h:
    spammy_function()
    result = h.autopsy()

(仅在Python 3中测试过)

编辑:现在可以选择 stderr stdout 或两者兼备,例如Hider([stdout,stderr])


这应该是Python 3的正确答案!还可以添加seld._stderr == sys.stderr; sys.stderr = self <...> - t-bltg

2

我的解决方案与你的类似,但是使用了contextlib,代码更加简短易懂(我个人认为)。

import contextlib


@contextlib.contextmanager
def stdchannel_redirected(stdchannel, dest_filename):
    """
    A context manager to temporarily redirect stdout or stderr

    e.g.:


    with stdchannel_redirected(sys.stderr, os.devnull):
        if compiler.has_function('clock_gettime', libraries=['rt']):
            libraries.append('rt')
    """

    try:
        oldstdchannel = os.dup(stdchannel.fileno())
        dest_file = open(dest_filename, 'w')
        os.dup2(dest_file.fileno(), stdchannel.fileno())

        yield
    finally:
        if oldstdchannel is not None:
            os.dup2(oldstdchannel, stdchannel.fileno())
        if dest_file is not None:
            dest_file.close()

我创建这个的背景可以在这篇博客文章中找到。和你的情况类似。
我在setup.py中这样使用它:
with stdchannel_redirected(sys.stderr, os.devnull):
    if compiler.has_function('clock_gettime', libraries=['rt']):
        libraries.append('rt')

1
我使用一个装饰器来实现这个。它保存了 sys.stdoutsys.stderr 的引用,并将这些变量指向 null。然后,在函数执行后,原始引用被检索。重要的是要注意 try/except 块,它允许在函数引发异常时检索到原始引用。
def suppress_std(func):
    def wrapper(*args, **kwargs):
        stderr_tmp = sys.stderr
        stdout_tmp = sys.stdout
        null = open(os.devnull, 'w')
        sys.stdout = null
        sys.stderr = null
        try:
            result = func(*args, **kwargs)
            sys.stderr = stderr_tmp
            sys.stdout = stdout_tmp
            return result
        except:
            sys.stderr = stderr_tmp
            sys.stdout = stdout_tmp
            raise
    return wrapper

使用方法:

@suppress_std
def function_std_suppressed():
    # code here

0

只需使用Linux/Unix操作系统:

./myscript.py 2>/dev/null # gets rid of stderr
./myscript.py 2>/somewhere/myerror.log

欢迎来到SO!如果您回答的是旧问题(这个问题已经超过8年了),并且已经有一个被接受的答案(这种情况就是这里的情况):请检查您是否真的有实质性的改进可以提供。如果没有,请不要回答。(您的答案已经给出。) - Timus

-2
如果您在Linux系统上运行此脚本,您应该能够:
$> ./runscript.py > output.txt

我不想禁止脚本生成的所有输出,只想抑制这些特定函数生成的无用输出。否则,是的,这将是最简单的解决方案... - jeremiahbuddha
这个有用吗:在特定函数之前和之后添加一个打印。使用正则表达式解析输出,以消除上面添加的打印之间的所有内容。 - GeneralBecos

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接