如何在Python中防止C共享库向stdout打印输出?

56

我使用一个导入C共享库的python库,该库会在stdout上打印信息。我希望获得整洁的输出以便将其通过管道使用或重定向到文件中。这些打印操作是在共享库之外完成的。

一开始,我的处理方式是:

# file: test.py
import os
from ctypes import *
from tempfile import mktemp

libc = CDLL("libc.so.6")

print # That's here on purpose, otherwise hello word is always printed

tempfile = open(mktemp(),'w')
savestdout = os.dup(1)
os.close(1)
if os.dup(tempfile.fileno()) != 1:
    assert False, "couldn't redirect stdout - dup() error"

# let's pretend this is a call to my library
libc.printf("hello world\n")

os.close(1)
os.dup(savestdout)
os.close(savestdout)

这个第一种方法有一半可行: - 不知何故,在移动stdout之前需要一个“print”语句,否则始终会打印hello word。结果,它将打印一个空行,而不是库通常输出的所有模糊内容。 - 更令人恼火的是,当重定向到文件时,它会失败:
$python test.py > foo && cat foo

hello world

我第二次尝试使用Python是受到评论区中给出的另一个类似主题的启发:

import os
import sys
from ctypes import *
libc = CDLL("libc.so.6")

devnull = open('/dev/null', 'w')
oldstdout = os.dup(sys.stdout.fileno())
os.dup2(devnull.fileno(), 1)

# We still pretend this is a call to my library
libc.printf("hello\n")

os.dup2(oldstdout, 1)

这个也无法阻止“hello”打印。

因为我觉得这有点低级,所以我决定完全使用ctypes。我从这个不打印任何东西的C程序中获得了灵感:

#include <stdio.h>

int main(int argc, const char *argv[]) {
    char buf[20];
    int saved_stdout = dup(1);
    freopen("/dev/null", "w", stdout);

    printf("hello\n"); // not printed

    sprintf(buf, "/dev/fd/%d", saved_stdout);
    freopen(buf, "w", stdout);

    return 0;
}

我建立了以下例子:

from ctypes import *
libc = CDLL("libc.so.6")

saved_stdout = libc.dup(1)
stdout = libc.fdopen(1, "w")
libc.freopen("/dev/null", "w", stdout);

libc.printf("hello\n")

libc.freopen("/dev/fd/" + str(saved_stdout), "w", stdout)

即使在printf之后,我libc.fflush(stdout)仍然会打印出“hello”。我开始觉得在Python中实现我想要的可能是不可能的。或者我获取标准输出文件指针的方式不正确。您认为呢?

4
你从哪里获取到段错误(堆栈跟踪)?而且,说实话...那个共享库的开发者做得真糟糕。在共享库内直接将内容写入标准输出流,却没有提供修改此行为的方法,这是很糟糕的。 - Axel
很不幸,我无法找到内部重定向 stdout 的任何方法。我认为你在正确的轨道上,可以通过用 C 包装共享库、将包装器制作成 dll 并使用 ctypes 调用该库来实现。我相信你的段错误是由于 sprintf 引起的,但我无法确定问题所在。 - Chinmay Kanchi
3
可能是抑制调用外部库的模块输出的重复问题。 - Ignacio Vazquez-Abrams
我不再遇到段错误了(而且我也不再使用sprintf),很抱歉让你的评论过时了,但我觉得这篇文章已经足够长了,没有必要加上堆栈跟踪。 - user48678
8个回答

43

根据@Yinon Ehrlich的答案,这个变体试图避免泄漏文件描述符:

import os
import sys
from contextlib import contextmanager

@contextmanager
def stdout_redirected(to=os.devnull):
    '''
    import os

    with stdout_redirected(to=filename):
        print("from Python")
        os.system("echo non-Python applications are also supported")
    '''
    fd = sys.stdout.fileno()

    ##### assert that Python and C stdio write using the same file descriptor
    ####assert libc.fileno(ctypes.c_void_p.in_dll(libc, "stdout")) == fd == 1

    def _redirect_stdout(to):
        sys.stdout.close() # + implicit flush()
        os.dup2(to.fileno(), fd) # fd writes to 'to' file
        sys.stdout = os.fdopen(fd, 'w') # Python writes to fd

    with os.fdopen(os.dup(fd), 'w') as old_stdout:
        with open(to, 'w') as file:
            _redirect_stdout(to=file)
        try:
            yield # allow code to be run with the redirected stdout
        finally:
            _redirect_stdout(to=old_stdout) # restore stdout.
                                            # buffering and flags such as
                                            # CLOEXEC may be different

标记为最佳答案,因为它没有泄漏。但是,我认为使用装饰器,像@Yinon Ehrlich更优雅。 - user48678
Sebastian:嗯,HideOutput确实是一个装饰器。用法是“with HideOutput:”。但我不知道contextlib.contextmanager。从文档中可以看出,它也是一个装饰器。我之前没注意到这一点。 - user48678
1
@user48678:你混淆了概念。with语句使用上下文管理器,例如:文件、锁、decimal.localcontext()(都是具有__enter____exit__方法的对象)。它们与装饰器不同,装饰器是可调用的(例如函数),接受一个可调用对象并返回一个可调用对象(通常如此),例如:staticmethodclassmethodmakebold,makeitalic。Python支持特殊的@语法来使用装饰器。 - jfs
1
没错,你说得对。我刚刚核实了事实,发现装饰器和我原来想的完全不同。对于我的错误表示歉意。 - user48678
@J.F. Sebastian:感谢您澄清文件描述符泄漏问题,并向我介绍了contextlib.contextmanager装饰器。 - Yinon Ehrlich
显示剩余17条评论

18

建议使用os.dup2而非os.dup,就像你的第二个想法一样。你的代码看起来有些绕弯子了。除了/dev/null,不要去碰/dev文件。这是不必要的。在这里写任何C语言也是不必要的。

关键是使用dup保存stdout文件描述符,然后将其传递给fdopen以创建新的sys.stdout Python对象。同时,打开一个指向/dev/null的文件描述符,并使用dup2覆盖现有的stdout文件描述符。然后关闭旧的/dev/null文件描述符。调用dup2是必要的,因为我们无法告诉open要返回哪个文件描述符,dup2实际上是唯一的方法。

编辑:如果你要重定向到文件,那么stdout就不是行缓冲的,所以你必须刷新它。你可以从Python中做到这一点,它会正确地与C交互。当然,如果在你往stdout中写入任何内容之前调用此函数,那么这就没关系了。

以下是我刚刚测试并在我的系统上运行成功的示例代码:

import zook
import os
import sys

def redirect_stdout():
    print "Redirecting stdout"
    sys.stdout.flush() # <--- important when redirecting to files
    newstdout = os.dup(1)
    devnull = os.open(os.devnull, os.O_WRONLY)
    os.dup2(devnull, 1)
    os.close(devnull)
    sys.stdout = os.fdopen(newstdout, 'w')

zook.myfunc()
redirect_stdout()
zook.myfunc()
print "But python can still print to stdout..."

"zook"模块是一个非常简单的C语言库。

#include <Python.h>
#include <stdio.h>

static PyObject *
myfunc(PyObject *self, PyObject *args)
{
    puts("myfunc called");
    Py_INCREF(Py_None);
    return Py_None;
}

static PyMethodDef zookMethods[] = {
    {"myfunc",  myfunc, METH_VARARGS, "Print a string."},
    {NULL, NULL, 0, NULL}
};

PyMODINIT_FUNC
initzook(void)
{
    (void)Py_InitModule("zook", zookMethods);
}

输出结果是什么?

$ python2.5 test.py
myfunc called
Redirecting stdout
But python can still print to stdout...

并且重定向到文件?

$ python2.5 test.py > test.txt
$ cat test.txt
myfunc called
Redirecting stdout
But python can still print to stdout...

2
感谢@YinonEhrlich建议使用os.devnull代替'/dev/null'。该编辑被审核员拒绝,我不同意这个决定。 - Dietrich Epp
这种方法能用来重定向至由日志模块创建的文件吗? - BigBrownBear00
理论上是可以的,但你怎么知道要替换哪个文件描述符?你怎么知道要刷新哪个对象?就像用炸药开门一样,因为你懒得找钥匙。 - Dietrich Epp
这就是问题的关键所在...是否可以使用Logger对象来完成,并且如何完成? - BigBrownBear00
我甚至不知道“Logger对象”是什么,这里可能有更大的讨论(比如,你想做什么?旋转日志?重定向日志?),我倾向于将其作为自己的问题来询问。 - Dietrich Epp

18

结合以下两个答案-https://dev59.com/82445IYBdhLWcg3wBVrz#5103455https://dev59.com/u2855IYBdhLWcg3wuG6W#4178672,创建一个上下文管理器仅在其作用域内阻止标准输出(print),其中第一个答案中的代码阻止了任何外部输出,而后一个答案在结尾处遗漏了sys.stdout.flush()。

class HideOutput(object):
    '''
    A context manager that block stdout for its scope, usage:

    with HideOutput():
        os.system('ls -l')
    '''

    def __init__(self, *args, **kw):
        sys.stdout.flush()
        self._origstdout = sys.stdout
        self._oldstdout_fno = os.dup(sys.stdout.fileno())
        self._devnull = os.open(os.devnull, os.O_WRONLY)

    def __enter__(self):
        self._newstdout = os.dup(1)
        os.dup2(self._devnull, 1)
        os.close(self._devnull)
        sys.stdout = os.fdopen(self._newstdout, 'w')

    def __exit__(self, exc_type, exc_val, exc_tb):
        sys.stdout = self._origstdout
        sys.stdout.flush()
        os.dup2(self._oldstdout_fno, 1)

这非常干净整洁。Dietrich Eppentry 是当我提出这个问题时解决我的问题的那一个,他仍然得到我的投票,但你的解决方法更加优雅。我将其标记为最佳答案。 - user48678
5
每次使用它时,它都会泄露文件描述符。任何使用 os.dup()os.open() 创建的描述符必须以某种方式关闭,但对于 _old_stdout_fileno,这种关闭并没有发生;如果您在上下文中不使用它,则 _devnull 也将泄漏。泄漏文件描述符非常严重,因为您只能获取大约256或1024个文件描述符。 - Dietrich Epp
1
@DietrichEpp:我已经发布了尝试避免泄漏文件描述符的解决方案 - jfs

4

以下是我最终的做法。希望这对其他人有用(这在我的Linux系统上有效)。

我自豪地介绍libshutup,旨在让外部库保持安静。

1)复制以下文件

// file: shutup.c
#include <stdio.h>
#include <unistd.h>

static char buf[20];
static int saved_stdout;

void stdout_off() {
    saved_stdout = dup(1);
    freopen("/dev/null", "w", stdout);
}

void stdout_on() {
    sprintf(buf, "/dev/fd/%d", saved_stdout);
    freopen(buf, "w", stdout);
}

2) 将其编译为共享库

gcc -Wall -shared shutup.c -fPIC -o libshutup.so

3) 在您的代码中像这样使用它

from ctypes import *
shutup = CDLL("libshutup.so")

shutup.stdout_off()

# Let's pretend this printf comes from the external lib
libc = CDLL("libc.so.6")
libc.printf("hello\n")

shutup.stdout_on()

所有这些只是为了替换掉 Python 的 5 行代码。 - Ignacio Vazquez-Abrams
1
如果你有更好的答案,我会非常高兴听取。你可以运行上面的每个示例并查看它们都打印了些什么。目前为止,这是唯一一个真正对我有效的解决方案。 - user48678
这个问题已经在与之重复的那个问题中得到解决。 - Ignacio Vazquez-Abrams
3
不是的。只需尝试运行我在第二次尝试中提供的代码片段,这恰好是您在帖子中给出的建议,然后您就会看到。 - user48678
很好的解决方案,它适用于我的特定情况。我之前找到的所有解决方案都没有完全起作用。我想做的一件事是将stdout作为char数组返回,以便在Python中进行控制,而不是写入“/dev/fd/%d”。有什么办法可以做到这一点吗?我对C和ctypes都非常陌生。如果这个问题太明显了,请原谅我的无知。 - 윤제균
显示剩余2条评论

4
这里的最佳答案非常好。但是,它需要使用sys.stdout.close(),这会与Jupyter冲突,如果使用Python笔记本,则无法正常使用。有一个很棒的项目叫做Wurlitzer,可以通过上下文管理器解决根本问题,不仅在Jupter中可用,还提供了本地Jupyer扩展。 https://github.com/minrk/wurlitzer https://pypi.org/project/wurlitzer/
pip install wurlitzer

from wurlitzer import pipes

with pipes() as (out, err):
    call_some_c_function()

stdout = out.read()

from io import StringIO
from wurlitzer import pipes, STDOUT

out = StringIO()
with pipes(stdout=out, stderr=STDOUT):
    call_some_c_function()

stdout = out.getvalue()

from wurlitzer import sys_pipes

with sys_pipes():
    call_some_c_function()

最神奇的部分是:它支持Jupyter:

%load_ext wurlitzer

0

jfs的答案给了我一个错误,所以我基于这个答案想出了另一个解决方案。

ValueError: I/O operation on closed file.

import contextlib

@contextlib.contextmanager
def silence_stderr():
    stderr_fd = sys.stderr.fileno()
    orig_fd = os.dup(stderr_fd)
    null_fd = os.open(os.devnull, os.O_WRONLY)
    os.dup2(null_fd, stderr_fd)
    try:
        yield
    finally:
        os.dup2(orig_fd, stderr_fd)
        os.close(orig_fd)
        os.close(null_fd)

使用起来非常简单,如预期所示。

with silence_stderr():
    # call python module: stderr will be silenced
    # call c/c++ library: stderr will be silenced

你可以通过简单的查找替换来轻松修改代码,将stdout静音而不是stderr


0

之前的所有答案对我都没有用,要么导致分段错误,要么不能正确关闭输出。复制this的答案并添加一个额外的关闭语句似乎不会泄漏任何文件描述符,并且在我的情况下起作用:

class HideOutput(object):
    '''
    A context manager that block stdout for its scope, usage:

    with HideOutput():
        os.system('ls -l')
    '''

    def __init__(self, *args, **kw):
        sys.stdout.flush()
        self._origstdout = sys.stdout
        self._oldstdout_fno = os.dup(sys.stdout.fileno())
        self._devnull = os.open(os.devnull, os.O_WRONLY)

    def __enter__(self):
        self._newstdout = os.dup(1)
        os.dup2(self._devnull, 1)
        os.close(self._devnull)
        sys.stdout = os.fdopen(self._newstdout, 'w')

    def __exit__(self, exc_type, exc_val, exc_tb):
        sys.stdout = self._origstdout
        sys.stdout.flush()
        os.dup2(self._oldstdout_fno, 1)
        os.close(self._oldstdout_fno) # Additional close to not leak fd

99%的代码是从this复制的,并且按照this答案中提到的方式关闭了文件。


-3

你不是可以像在Python中那样做吗?导入sys并将sys.stdout和sys.stderr指向不是默认sys.stdout和sys.stderr的内容?我在一些应用程序中经常这样做,需要从库中获取输出。


不行,因为 sys.std* 只被 Python 代码使用,而 C 库直接使用 FDs 0 到 2。 - Ignacio Vazquez-Abrams
2
这应该是一条注释,而不是一个答案。 - Will
实际上,这是一个答案,但没有工作代码,我可以看出它没有传达信息。回到2011年,我没有意识到在StackOverflow上你必须做多少手把手的指导。无论如何,不确定你从挑毛病和给它一个反对票中得到了什么...2.5年后。 - Jeremy Whitlock

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